Home | History | Annotate | Download | only in cli
      1 # Copyright 2016 The TensorFlow Authors. All Rights Reserved.
      2 #
      3 # Licensed under the Apache License, Version 2.0 (the "License");
      4 # you may not use this file except in compliance with the License.
      5 # You may obtain a copy of the License at
      6 #
      7 #     http://www.apache.org/licenses/LICENSE-2.0
      8 #
      9 # Unless required by applicable law or agreed to in writing, software
     10 # distributed under the License is distributed on an "AS IS" BASIS,
     11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 # See the License for the specific language governing permissions and
     13 # limitations under the License.
     14 # ==============================================================================
     15 """Curses-Based Command-Line Interface of TensorFlow Debugger (tfdbg)."""
     16 from __future__ import absolute_import
     17 from __future__ import division
     18 from __future__ import print_function
     19 
     20 import collections
     21 import curses
     22 from curses import textpad
     23 import os
     24 import signal
     25 import sys
     26 import threading
     27 
     28 from six.moves import xrange  # pylint: disable=redefined-builtin
     29 
     30 from tensorflow.python.debug.cli import base_ui
     31 from tensorflow.python.debug.cli import cli_shared
     32 from tensorflow.python.debug.cli import command_parser
     33 from tensorflow.python.debug.cli import curses_widgets
     34 from tensorflow.python.debug.cli import debugger_cli_common
     35 from tensorflow.python.debug.cli import tensor_format
     36 
     37 
     38 _SCROLL_REFRESH = "refresh"
     39 _SCROLL_UP = "up"
     40 _SCROLL_DOWN = "down"
     41 _SCROLL_UP_A_LINE = "up_a_line"
     42 _SCROLL_DOWN_A_LINE = "down_a_line"
     43 _SCROLL_HOME = "home"
     44 _SCROLL_END = "end"
     45 _SCROLL_TO_LINE_INDEX = "scroll_to_line_index"
     46 
     47 _COLOR_READY_COLORTERMS = ["gnome-terminal", "xfce4-terminal"]
     48 _COLOR_ENABLED_TERM = "xterm-256color"
     49 
     50 
     51 def _get_command_from_line_attr_segs(mouse_x, attr_segs):
     52   """Attempt to extract command from the attribute segments of a line.
     53 
     54   Args:
     55     mouse_x: (int) x coordinate of the mouse event.
     56     attr_segs: (list) The list of attribute segments of a line from a
     57       RichTextLines object.
     58 
     59   Returns:
     60     (str or None) If a command exists: the command as a str; otherwise, None.
     61   """
     62 
     63   for seg in attr_segs:
     64     if seg[0] <= mouse_x < seg[1]:
     65       attributes = seg[2] if isinstance(seg[2], list) else [seg[2]]
     66       for attr in attributes:
     67         if isinstance(attr, debugger_cli_common.MenuItem):
     68           return attr.content
     69 
     70 
     71 class ScrollBar(object):
     72   """Vertical ScrollBar for Curses-based CLI.
     73 
     74   An object of this class has knowledge of the location of the scroll bar
     75   in the screen coordinates, the current scrolling position, and the total
     76   number of text lines in the screen text. By using this information, it
     77   can generate text rendering of the scroll bar, which consists of and UP
     78   button on the top and a DOWN button on the bottom, in addition to a scroll
     79   block in between, whose exact location is determined by the scrolling
     80   position. The object can also calculate the scrolling command (e.g.,
     81   _SCROLL_UP_A_LINE, _SCROLL_DOWN) from the coordinate of a mouse click
     82   event in the screen region it occupies.
     83   """
     84 
     85   BASE_ATTR = cli_shared.COLOR_BLACK + "_on_" + cli_shared.COLOR_WHITE
     86 
     87   def __init__(self,
     88                min_x,
     89                min_y,
     90                max_x,
     91                max_y,
     92                scroll_position,
     93                output_num_rows):
     94     """Constructor of ScrollBar.
     95 
     96     Args:
     97       min_x: (int) left index of the scroll bar on the screen (inclusive).
     98       min_y: (int) top index of the scroll bar on the screen (inclusive).
     99       max_x: (int) right index of the scroll bar on the screen (inclusive).
    100       max_y: (int) bottom index of the scroll bar on the screen (inclusive).
    101       scroll_position: (int) 0-based location of the screen output. For example,
    102         if the screen output is scrolled to the top, the value of
    103         scroll_position should be 0. If it is scrolled to the bottom, the value
    104         should be output_num_rows - 1.
    105       output_num_rows: (int) Total number of output rows.
    106 
    107     Raises:
    108       ValueError: If the width or height of the scroll bar, as determined
    109        by min_x, max_x, min_y and max_y, is too small.
    110     """
    111 
    112     self._min_x = min_x
    113     self._min_y = min_y
    114     self._max_x = max_x
    115     self._max_y = max_y
    116     self._scroll_position = scroll_position
    117     self._output_num_rows = output_num_rows
    118     self._scroll_bar_height = max_y - min_y + 1
    119 
    120     if self._max_x < self._min_x:
    121       raise ValueError("Insufficient width for ScrollBar (%d)" %
    122                        (self._max_x - self._min_x + 1))
    123     if self._max_y < self._min_y + 3:
    124       raise ValueError("Insufficient height for ScrollBar (%d)" %
    125                        (self._max_y - self._min_y + 1))
    126 
    127   def _block_y(self, screen_coord_sys=False):
    128     """Get the 0-based y coordinate of the scroll block.
    129 
    130     This y coordinate takes into account the presence of the UP and DN buttons
    131     present at the top and bottom of the ScrollBar. For example, at the home
    132     location, the return value will be 1; at the bottom location, the return
    133     value will be self._scroll_bar_height - 2.
    134 
    135     Args:
    136       screen_coord_sys: (`bool`) whether the return value will be in the
    137         screen coordinate system.
    138 
    139     Returns:
    140       (int) 0-based y coordinate of the scroll block, in the ScrollBar
    141         coordinate system by default. For example,
    142         when scroll position is at the top, this return value will be 1 (not 0,
    143         because of the presence of the UP button). When scroll position is at
    144         the bottom, this return value will be self._scroll_bar_height - 2
    145         (not self._scroll_bar_height - 1, because of the presence of the DOWN
    146         button).
    147     """
    148 
    149     rel_block_y = int(
    150         float(self._scroll_position) / (self._output_num_rows - 1) *
    151         (self._scroll_bar_height - 3)) + 1
    152     return rel_block_y + self._min_y if screen_coord_sys else rel_block_y
    153 
    154   def layout(self):
    155     """Get the RichTextLines layout of the scroll bar.
    156 
    157     Returns:
    158       (debugger_cli_common.RichTextLines) The text layout of the scroll bar.
    159     """
    160     width = self._max_x - self._min_x + 1
    161     empty_line = " " * width
    162     foreground_font_attr_segs = [(0, width, self.BASE_ATTR)]
    163 
    164     if self._output_num_rows > 1:
    165       block_y = self._block_y()
    166 
    167       if width == 1:
    168         up_text = "U"
    169         down_text = "D"
    170       elif width == 2:
    171         up_text = "UP"
    172         down_text = "DN"
    173       elif width == 3:
    174         up_text = "UP "
    175         down_text = "DN "
    176       else:
    177         up_text = " UP "
    178         down_text = "DOWN"
    179 
    180       layout = debugger_cli_common.RichTextLines(
    181           [up_text], font_attr_segs={0: [(0, width, self.BASE_ATTR)]})
    182       for i in xrange(1, self._scroll_bar_height - 1):
    183         font_attr_segs = foreground_font_attr_segs if i == block_y else None
    184         layout.append(empty_line, font_attr_segs=font_attr_segs)
    185       layout.append(down_text, font_attr_segs=foreground_font_attr_segs)
    186     else:
    187       layout = debugger_cli_common.RichTextLines(
    188           [empty_line] * self._scroll_bar_height)
    189 
    190     return layout
    191 
    192   def get_click_command(self, mouse_y):
    193     if self._output_num_rows <= 1:
    194       return None
    195     elif mouse_y == self._min_y:
    196       return _SCROLL_UP_A_LINE
    197     elif mouse_y == self._max_y:
    198       return _SCROLL_DOWN_A_LINE
    199     elif (mouse_y > self._block_y(screen_coord_sys=True) and
    200           mouse_y < self._max_y):
    201       return _SCROLL_DOWN
    202     elif (mouse_y < self._block_y(screen_coord_sys=True) and
    203           mouse_y > self._min_y):
    204       return _SCROLL_UP
    205     else:
    206       return None
    207 
    208 
    209 class CursesUI(base_ui.BaseUI):
    210   """Curses-based Command-line UI.
    211 
    212   In this class, the methods with the prefix "_screen_" are the methods that
    213   interact with the actual terminal using the curses library.
    214   """
    215 
    216   CLI_TERMINATOR_KEY = 7  # Terminator key for input text box.
    217   CLI_TAB_KEY = ord("\t")
    218   BACKSPACE_KEY = ord("\b")
    219   REGEX_SEARCH_PREFIX = "/"
    220   TENSOR_INDICES_NAVIGATION_PREFIX = "@"
    221 
    222   _NAVIGATION_FORWARD_COMMAND = "next"
    223   _NAVIGATION_BACK_COMMAND = "prev"
    224 
    225   # Limit screen width to work around the limitation of the curses library that
    226   # it may return invalid x coordinates for large values.
    227   _SCREEN_WIDTH_LIMIT = 220
    228 
    229   # Possible Enter keys. 343 is curses key code for the num-pad Enter key when
    230   # num lock is off.
    231   CLI_CR_KEYS = [ord("\n"), ord("\r"), 343]
    232 
    233   _KEY_MAP = {
    234       127: curses.KEY_BACKSPACE,  # Backspace
    235       curses.KEY_DC: 4,  # Delete
    236   }
    237 
    238   _FOREGROUND_COLORS = {
    239       cli_shared.COLOR_WHITE: curses.COLOR_WHITE,
    240       cli_shared.COLOR_RED: curses.COLOR_RED,
    241       cli_shared.COLOR_GREEN: curses.COLOR_GREEN,
    242       cli_shared.COLOR_YELLOW: curses.COLOR_YELLOW,
    243       cli_shared.COLOR_BLUE: curses.COLOR_BLUE,
    244       cli_shared.COLOR_CYAN: curses.COLOR_CYAN,
    245       cli_shared.COLOR_MAGENTA: curses.COLOR_MAGENTA,
    246       cli_shared.COLOR_BLACK: curses.COLOR_BLACK,
    247   }
    248   _BACKGROUND_COLORS = {
    249       "transparent": -1,
    250       cli_shared.COLOR_WHITE: curses.COLOR_WHITE,
    251       cli_shared.COLOR_BLACK: curses.COLOR_BLACK,
    252   }
    253 
    254   # Font attribute for search and highlighting.
    255   _SEARCH_HIGHLIGHT_FONT_ATTR = (
    256       cli_shared.COLOR_BLACK + "_on_" + cli_shared.COLOR_WHITE)
    257   _ARRAY_INDICES_COLOR_PAIR = (
    258       cli_shared.COLOR_BLACK + "_on_" + cli_shared.COLOR_WHITE)
    259   _ERROR_TOAST_COLOR_PAIR = (
    260       cli_shared.COLOR_RED + "_on_" + cli_shared.COLOR_WHITE)
    261   _INFO_TOAST_COLOR_PAIR = (
    262       cli_shared.COLOR_BLUE + "_on_" + cli_shared.COLOR_WHITE)
    263   _STATUS_BAR_COLOR_PAIR = (
    264       cli_shared.COLOR_BLACK + "_on_" + cli_shared.COLOR_WHITE)
    265   _UI_WAIT_COLOR_PAIR = (
    266       cli_shared.COLOR_MAGENTA + "_on_" + cli_shared.COLOR_WHITE)
    267   _NAVIGATION_WARNING_COLOR_PAIR = (
    268       cli_shared.COLOR_RED + "_on_" + cli_shared.COLOR_WHITE)
    269 
    270   _UI_WAIT_MESSAGE = "Processing..."
    271 
    272   # The delay (in ms) between each update of the scroll bar when the mouse
    273   # button is held down on the scroll bar. Controls how fast the screen scrolls.
    274   _MOUSE_SCROLL_DELAY_MS = 100
    275 
    276   _single_instance_lock = threading.Lock()
    277 
    278   def __init__(self, on_ui_exit=None, config=None):
    279     """Constructor of CursesUI.
    280 
    281     Args:
    282       on_ui_exit: (Callable) Callback invoked when the UI exits.
    283       config: An instance of `cli_config.CLIConfig()` carrying user-facing
    284         configurations.
    285     """
    286 
    287     base_ui.BaseUI.__init__(self, on_ui_exit=on_ui_exit, config=config)
    288 
    289     self._screen_init()
    290     self._screen_refresh_size()
    291     # TODO(cais): Error out if the size of the screen is too small.
    292 
    293     # Initialize some UI component size and locations.
    294     self._init_layout()
    295 
    296     self._command_history_store = debugger_cli_common.CommandHistory()
    297 
    298     # Active list of command history, used in history navigation.
    299     # _command_handler_registry holds all the history commands the CLI has
    300     # received, up to a size limit. _active_command_history is the history
    301     # currently being navigated in, e.g., using the Up/Down keys. The latter
    302     # can be different from the former during prefixed or regex-based history
    303     # navigation, e.g., when user enter the beginning of a command and hit Up.
    304     self._active_command_history = []
    305 
    306     # Pointer to the current position in the history sequence.
    307     # 0 means it is a new command being keyed in.
    308     self._command_pointer = 0
    309 
    310     self._command_history_limit = 100
    311 
    312     self._pending_command = ""
    313 
    314     self._nav_history = curses_widgets.CursesNavigationHistory(10)
    315 
    316     # State related to screen output.
    317     self._output_pad = None
    318     self._output_pad_row = 0
    319     self._output_array_pointer_indices = None
    320     self._curr_unwrapped_output = None
    321     self._curr_wrapped_output = None
    322 
    323     try:
    324       # Register signal handler for SIGINT.
    325       signal.signal(signal.SIGINT, self._interrupt_handler)
    326     except ValueError:
    327       # Running in a child thread, can't catch signals.
    328       pass
    329 
    330     self.register_command_handler(
    331         "mouse",
    332         self._mouse_mode_command_handler,
    333         "Get or set the mouse mode of this CLI: (on|off)",
    334         prefix_aliases=["m"])
    335 
    336   def _init_layout(self):
    337     """Initialize the layout of UI components.
    338 
    339     Initialize the location and size of UI components such as command textbox
    340     and output region according to the terminal size.
    341     """
    342 
    343     # NamedTuple for rectangular locations on screen
    344     self.rectangle = collections.namedtuple("rectangle",
    345                                             "top left bottom right")
    346 
    347     # Height of command text box
    348     self._command_textbox_height = 2
    349 
    350     self._title_row = 0
    351 
    352     # Row index of the Navigation Bar (i.e., the bar that contains forward and
    353     # backward buttons and displays the current command line).
    354     self._nav_bar_row = 1
    355 
    356     # Top row index of the output pad.
    357     # A "pad" is a curses object that holds lines of text and not limited to
    358     # screen size. It can be rendered on the screen partially with scroll
    359     # parameters specified.
    360     self._output_top_row = 2
    361 
    362     # Number of rows that the output pad has.
    363     self._output_num_rows = (
    364         self._max_y - self._output_top_row - self._command_textbox_height - 1)
    365 
    366     # Row index of scroll information line: Taking into account the zero-based
    367     # row indexing and the command textbox area under the scroll information
    368     # row.
    369     self._output_scroll_row = self._max_y - 1 - self._command_textbox_height
    370 
    371     # Tab completion bottom row.
    372     self._candidates_top_row = self._output_scroll_row - 4
    373     self._candidates_bottom_row = self._output_scroll_row - 1
    374 
    375     # Maximum number of lines the candidates display can have.
    376     self._candidates_max_lines = int(self._output_num_rows / 2)
    377 
    378     self.max_output_lines = 10000
    379 
    380     # Regex search state.
    381     self._curr_search_regex = None
    382     self._unwrapped_regex_match_lines = []
    383 
    384     # Size of view port on screen, which is always smaller or equal to the
    385     # screen size.
    386     self._output_pad_screen_height = self._output_num_rows - 1
    387     self._output_pad_screen_width = self._max_x - 2
    388     self._output_pad_screen_location = self.rectangle(
    389         top=self._output_top_row,
    390         left=0,
    391         bottom=self._output_top_row + self._output_num_rows,
    392         right=self._output_pad_screen_width)
    393 
    394   def _screen_init(self):
    395     """Screen initialization.
    396 
    397     Creates curses stdscr and initialize the color pairs for display.
    398     """
    399     # If the terminal type is color-ready, enable it.
    400     if os.getenv("COLORTERM") in _COLOR_READY_COLORTERMS:
    401       os.environ["TERM"] = _COLOR_ENABLED_TERM
    402     self._stdscr = curses.initscr()
    403     self._command_window = None
    404     self._screen_color_init()
    405 
    406   def _screen_color_init(self):
    407     """Initialization of screen colors."""
    408     curses.start_color()
    409     curses.use_default_colors()
    410     self._color_pairs = {}
    411     color_index = 0
    412 
    413     # Prepare color pairs.
    414     for fg_color in self._FOREGROUND_COLORS:
    415       for bg_color in self._BACKGROUND_COLORS:
    416         color_index += 1
    417         curses.init_pair(color_index, self._FOREGROUND_COLORS[fg_color],
    418                          self._BACKGROUND_COLORS[bg_color])
    419 
    420         color_name = fg_color
    421         if bg_color != "transparent":
    422           color_name += "_on_" + bg_color
    423 
    424         self._color_pairs[color_name] = curses.color_pair(color_index)
    425 
    426     # Try getting color(s) available only under 256-color support.
    427     try:
    428       color_index += 1
    429       curses.init_pair(color_index, 245, -1)
    430       self._color_pairs[cli_shared.COLOR_GRAY] = curses.color_pair(color_index)
    431     except curses.error:
    432       # Use fall-back color(s):
    433       self._color_pairs[cli_shared.COLOR_GRAY] = (
    434           self._color_pairs[cli_shared.COLOR_GREEN])
    435 
    436     # A_BOLD or A_BLINK is not really a "color". But place it here for
    437     # convenience.
    438     self._color_pairs["bold"] = curses.A_BOLD
    439     self._color_pairs["blink"] = curses.A_BLINK
    440     self._color_pairs["underline"] = curses.A_UNDERLINE
    441 
    442     # Default color pair to use when a specified color pair does not exist.
    443     self._default_color_pair = self._color_pairs[cli_shared.COLOR_WHITE]
    444 
    445   def _screen_launch(self, enable_mouse_on_start):
    446     """Launch the curses screen."""
    447 
    448     curses.noecho()
    449     curses.cbreak()
    450     self._stdscr.keypad(1)
    451 
    452     self._mouse_enabled = self.config.get("mouse_mode")
    453     self._screen_set_mousemask()
    454     self.config.set_callback(
    455         "mouse_mode",
    456         lambda cfg: self._set_mouse_enabled(cfg.get("mouse_mode")))
    457 
    458     self._screen_create_command_window()
    459 
    460   def _screen_create_command_window(self):
    461     """Create command window according to screen size."""
    462     if self._command_window:
    463       del self._command_window
    464 
    465     self._command_window = curses.newwin(
    466         self._command_textbox_height, self._max_x - len(self.CLI_PROMPT),
    467         self._max_y - self._command_textbox_height, len(self.CLI_PROMPT))
    468 
    469   def _screen_refresh(self):
    470     self._stdscr.refresh()
    471 
    472   def _screen_terminate(self):
    473     """Terminate the curses screen."""
    474 
    475     self._stdscr.keypad(0)
    476     curses.nocbreak()
    477     curses.echo()
    478     curses.endwin()
    479 
    480     try:
    481       # Remove SIGINT handler.
    482       signal.signal(signal.SIGINT, signal.SIG_DFL)
    483     except ValueError:
    484      # Can't catch signals unless you're the main thread.
    485       pass
    486 
    487   def run_ui(self,
    488              init_command=None,
    489              title=None,
    490              title_color=None,
    491              enable_mouse_on_start=True):
    492     """Run the CLI: See the doc of base_ui.BaseUI.run_ui for more details."""
    493 
    494     # Only one instance of the Curses UI can be running at a time, since
    495     # otherwise they would try to both read from the same keystrokes, and write
    496     # to the same screen.
    497     self._single_instance_lock.acquire()
    498 
    499     self._screen_launch(enable_mouse_on_start=enable_mouse_on_start)
    500 
    501     # Optional initial command.
    502     if init_command is not None:
    503       self._dispatch_command(init_command)
    504 
    505     if title is not None:
    506       self._title(title, title_color=title_color)
    507 
    508     # CLI main loop.
    509     exit_token = self._ui_loop()
    510 
    511     if self._on_ui_exit:
    512       self._on_ui_exit()
    513 
    514     self._screen_terminate()
    515 
    516     self._single_instance_lock.release()
    517 
    518     return exit_token
    519 
    520   def get_help(self):
    521     return self._command_handler_registry.get_help()
    522 
    523   def _addstr(self, *args):
    524     try:
    525       self._stdscr.addstr(*args)
    526     except curses.error:
    527       pass
    528 
    529   def _refresh_pad(self, pad, *args):
    530     try:
    531       pad.refresh(*args)
    532     except curses.error:
    533       pass
    534 
    535   def _screen_create_command_textbox(self, existing_command=None):
    536     """Create command textbox on screen.
    537 
    538     Args:
    539       existing_command: (str) A command string to put in the textbox right
    540         after its creation.
    541     """
    542 
    543     # Display the tfdbg prompt.
    544     self._addstr(self._max_y - self._command_textbox_height, 0,
    545                  self.CLI_PROMPT, curses.A_BOLD)
    546     self._stdscr.refresh()
    547 
    548     self._command_window.clear()
    549 
    550     # Command text box.
    551     self._command_textbox = textpad.Textbox(
    552         self._command_window, insert_mode=True)
    553 
    554     # Enter existing command.
    555     self._auto_key_in(existing_command)
    556 
    557   def _ui_loop(self):
    558     """Command-line UI loop.
    559 
    560     Returns:
    561       An exit token of arbitrary type. The token can be None.
    562     """
    563 
    564     while True:
    565       # Enter history command if pointer is in history (> 0):
    566       if self._command_pointer > 0:
    567         existing_command = self._active_command_history[-self._command_pointer]
    568       else:
    569         existing_command = self._pending_command
    570       self._screen_create_command_textbox(existing_command)
    571 
    572       try:
    573         command, terminator, pending_command_changed = self._get_user_command()
    574       except debugger_cli_common.CommandLineExit as e:
    575         return e.exit_token
    576 
    577       if not command and terminator != self.CLI_TAB_KEY:
    578         continue
    579 
    580       if terminator in self.CLI_CR_KEYS or terminator == curses.KEY_MOUSE:
    581         exit_token = self._dispatch_command(command)
    582         if exit_token is not None:
    583           return exit_token
    584       elif terminator == self.CLI_TAB_KEY:
    585         tab_completed = self._tab_complete(command)
    586         self._pending_command = tab_completed
    587         self._cmd_ptr = 0
    588       elif pending_command_changed:
    589         self._pending_command = command
    590 
    591     return
    592 
    593   def _get_user_command(self):
    594     """Get user command from UI.
    595 
    596     Returns:
    597       command: (str) The user-entered command.
    598       terminator: (str) Terminator type for the command.
    599         If command is a normal command entered with the Enter key, the value
    600         will be the key itself. If this is a tab completion call (using the
    601         Tab key), the value will reflect that as well.
    602       pending_command_changed:  (bool) If the pending command has changed.
    603         Used during command history navigation.
    604     """
    605 
    606     # First, reset textbox state variables.
    607     self._textbox_curr_terminator = None
    608     self._textbox_pending_command_changed = False
    609 
    610     command = self._screen_get_user_command()
    611     command = self._strip_terminator(command)
    612     return (command, self._textbox_curr_terminator,
    613             self._textbox_pending_command_changed)
    614 
    615   def _screen_get_user_command(self):
    616     return self._command_textbox.edit(validate=self._on_textbox_keypress)
    617 
    618   def _strip_terminator(self, command):
    619     if not command:
    620       return command
    621 
    622     for v in self.CLI_CR_KEYS:
    623       if v < 256:
    624         command = command.replace(chr(v), "")
    625 
    626     return command.strip()
    627 
    628   def _screen_refresh_size(self):
    629     self._max_y, self._max_x = self._stdscr.getmaxyx()
    630     if self._max_x > self._SCREEN_WIDTH_LIMIT:
    631       self._max_x = self._SCREEN_WIDTH_LIMIT
    632 
    633   def _navigate_screen_output(self, command):
    634     """Navigate in screen output history.
    635 
    636     Args:
    637       command: (`str`) the navigation command, from
    638         {self._NAVIGATION_FORWARD_COMMAND, self._NAVIGATION_BACK_COMMAND}.
    639     """
    640     if command == self._NAVIGATION_FORWARD_COMMAND:
    641       if self._nav_history.can_go_forward():
    642         item = self._nav_history.go_forward()
    643         scroll_position = item.scroll_position
    644       else:
    645         self._toast("At the LATEST in navigation history!",
    646                     color=self._NAVIGATION_WARNING_COLOR_PAIR)
    647         return
    648     else:
    649       if self._nav_history.can_go_back():
    650         item = self._nav_history.go_back()
    651         scroll_position = item.scroll_position
    652       else:
    653         self._toast("At the OLDEST in navigation history!",
    654                     color=self._NAVIGATION_WARNING_COLOR_PAIR)
    655         return
    656 
    657     self._display_output(item.screen_output)
    658     if scroll_position != 0:
    659       self._scroll_output(_SCROLL_TO_LINE_INDEX, line_index=scroll_position)
    660 
    661   def _dispatch_command(self, command):
    662     """Dispatch user command.
    663 
    664     Args:
    665       command: (str) Command to dispatch.
    666 
    667     Returns:
    668       An exit token object. None value means that the UI loop should not exit.
    669       A non-None value means the UI loop should exit.
    670     """
    671 
    672     if self._output_pad:
    673       self._toast(self._UI_WAIT_MESSAGE, color=self._UI_WAIT_COLOR_PAIR)
    674 
    675     if command in self.CLI_EXIT_COMMANDS:
    676       # Explicit user command-triggered exit: EXPLICIT_USER_EXIT as the exit
    677       # token.
    678       return debugger_cli_common.EXPLICIT_USER_EXIT
    679     elif (command == self._NAVIGATION_FORWARD_COMMAND or
    680           command == self._NAVIGATION_BACK_COMMAND):
    681       self._navigate_screen_output(command)
    682       return
    683 
    684     if command:
    685       self._command_history_store.add_command(command)
    686 
    687     if (command.startswith(self.REGEX_SEARCH_PREFIX) and
    688         self._curr_unwrapped_output):
    689       if len(command) > len(self.REGEX_SEARCH_PREFIX):
    690         # Command is like "/regex". Perform regex search.
    691         regex = command[len(self.REGEX_SEARCH_PREFIX):]
    692 
    693         self._curr_search_regex = regex
    694         self._display_output(self._curr_unwrapped_output, highlight_regex=regex)
    695       elif self._unwrapped_regex_match_lines:
    696         # Command is "/". Continue scrolling down matching lines.
    697         self._display_output(
    698             self._curr_unwrapped_output,
    699             is_refresh=True,
    700             highlight_regex=self._curr_search_regex)
    701 
    702       self._command_pointer = 0
    703       self._pending_command = ""
    704       return
    705     elif command.startswith(self.TENSOR_INDICES_NAVIGATION_PREFIX):
    706       indices_str = command[1:].strip()
    707       if indices_str:
    708         try:
    709           indices = command_parser.parse_indices(indices_str)
    710           omitted, line_index, _, _ = tensor_format.locate_tensor_element(
    711               self._curr_wrapped_output, indices)
    712           if not omitted:
    713             self._scroll_output(
    714                 _SCROLL_TO_LINE_INDEX, line_index=line_index)
    715         except Exception as e:  # pylint: disable=broad-except
    716           self._error_toast(str(e))
    717       else:
    718         self._error_toast("Empty indices.")
    719 
    720       return
    721 
    722     try:
    723       prefix, args, output_file_path = self._parse_command(command)
    724     except SyntaxError as e:
    725       self._error_toast(str(e))
    726       return
    727 
    728     if not prefix:
    729       # Empty command: take no action. Should not exit.
    730       return
    731 
    732     # Take into account scroll bar width.
    733     screen_info = {"cols": self._max_x - 2}
    734     exit_token = None
    735     if self._command_handler_registry.is_registered(prefix):
    736       try:
    737         screen_output = self._command_handler_registry.dispatch_command(
    738             prefix, args, screen_info=screen_info)
    739       except debugger_cli_common.CommandLineExit as e:
    740         exit_token = e.exit_token
    741     else:
    742       screen_output = debugger_cli_common.RichTextLines([
    743           self.ERROR_MESSAGE_PREFIX + "Invalid command prefix \"%s\"" % prefix
    744       ])
    745 
    746     # Clear active command history. Until next up/down history navigation
    747     # occurs, it will stay empty.
    748     self._active_command_history = []
    749 
    750     if exit_token is not None:
    751       return exit_token
    752 
    753     self._nav_history.add_item(command, screen_output, 0)
    754 
    755     self._display_output(screen_output)
    756     if output_file_path:
    757       try:
    758         screen_output.write_to_file(output_file_path)
    759         self._info_toast("Wrote output to %s" % output_file_path)
    760       except Exception:  # pylint: disable=broad-except
    761         self._error_toast("Failed to write output to %s" % output_file_path)
    762 
    763     self._command_pointer = 0
    764     self._pending_command = ""
    765 
    766   def _screen_gather_textbox_str(self):
    767     """Gather the text string in the command text box.
    768 
    769     Returns:
    770       (str) the current text string in the command textbox, excluding any
    771       return keys.
    772     """
    773 
    774     txt = self._command_textbox.gather()
    775     return txt.strip()
    776 
    777   def _on_textbox_keypress(self, x):
    778     """Text box key validator: Callback of key strokes.
    779 
    780     Handles a user's keypress in the input text box. Translates certain keys to
    781     terminator keys for the textbox to allow its edit() method to return.
    782     Also handles special key-triggered events such as PgUp/PgDown scrolling of
    783     the screen output.
    784 
    785     Args:
    786       x: (int) Key code.
    787 
    788     Returns:
    789       (int) A translated key code. In most cases, this is identical to the
    790         input x. However, if x is a Return key, the return value will be
    791         CLI_TERMINATOR_KEY, so that the text box's edit() method can return.
    792 
    793     Raises:
    794       TypeError: If the input x is not of type int.
    795       debugger_cli_common.CommandLineExit: If a mouse-triggered command returns
    796         an exit token when dispatched.
    797     """
    798     if not isinstance(x, int):
    799       raise TypeError("Key validator expected type int, received type %s" %
    800                       type(x))
    801 
    802     if x in self.CLI_CR_KEYS:
    803       # Make Enter key the terminator
    804       self._textbox_curr_terminator = x
    805       return self.CLI_TERMINATOR_KEY
    806     elif x == self.CLI_TAB_KEY:
    807       self._textbox_curr_terminator = self.CLI_TAB_KEY
    808       return self.CLI_TERMINATOR_KEY
    809     elif x == curses.KEY_PPAGE:
    810       self._scroll_output(_SCROLL_UP_A_LINE)
    811       return x
    812     elif x == curses.KEY_NPAGE:
    813       self._scroll_output(_SCROLL_DOWN_A_LINE)
    814       return x
    815     elif x == curses.KEY_HOME:
    816       self._scroll_output(_SCROLL_HOME)
    817       return x
    818     elif x == curses.KEY_END:
    819       self._scroll_output(_SCROLL_END)
    820       return x
    821     elif x in [curses.KEY_UP, curses.KEY_DOWN]:
    822       # Command history navigation.
    823       if not self._active_command_history:
    824         hist_prefix = self._screen_gather_textbox_str()
    825         self._active_command_history = (
    826             self._command_history_store.lookup_prefix(
    827                 hist_prefix, self._command_history_limit))
    828 
    829       if self._active_command_history:
    830         if x == curses.KEY_UP:
    831           if self._command_pointer < len(self._active_command_history):
    832             self._command_pointer += 1
    833         elif x == curses.KEY_DOWN:
    834           if self._command_pointer > 0:
    835             self._command_pointer -= 1
    836       else:
    837         self._command_pointer = 0
    838 
    839       self._textbox_curr_terminator = x
    840 
    841       # Force return from the textbox edit(), so that the textbox can be
    842       # redrawn with a history command entered.
    843       return self.CLI_TERMINATOR_KEY
    844     elif x == curses.KEY_RESIZE:
    845       # Respond to terminal resize.
    846       self._screen_refresh_size()
    847       self._init_layout()
    848       self._screen_create_command_window()
    849       self._redraw_output()
    850 
    851       # Force return from the textbox edit(), so that the textbox can be
    852       # redrawn.
    853       return self.CLI_TERMINATOR_KEY
    854     elif x == curses.KEY_MOUSE and self._mouse_enabled:
    855       try:
    856         _, mouse_x, mouse_y, _, mouse_event_type = self._screen_getmouse()
    857       except curses.error:
    858         mouse_event_type = None
    859 
    860       if mouse_event_type == curses.BUTTON1_PRESSED:
    861         # Logic for held mouse-triggered scrolling.
    862         if mouse_x >= self._max_x - 2:
    863           # Disable blocking on checking for user input.
    864           self._command_window.nodelay(True)
    865 
    866           # Loop while mouse button is pressed.
    867           while mouse_event_type == curses.BUTTON1_PRESSED:
    868             # Sleep for a bit.
    869             curses.napms(self._MOUSE_SCROLL_DELAY_MS)
    870             scroll_command = self._scroll_bar.get_click_command(mouse_y)
    871             if scroll_command in (_SCROLL_UP_A_LINE, _SCROLL_DOWN_A_LINE):
    872               self._scroll_output(scroll_command)
    873 
    874             # Check to see if different mouse event is in queue.
    875             self._command_window.getch()
    876             try:
    877               _, _, _, _, mouse_event_type = self._screen_getmouse()
    878             except curses.error:
    879               pass
    880 
    881           self._command_window.nodelay(False)
    882           return x
    883       elif mouse_event_type == curses.BUTTON1_RELEASED:
    884         # Logic for mouse-triggered scrolling.
    885         if mouse_x >= self._max_x - 2:
    886           scroll_command = self._scroll_bar.get_click_command(mouse_y)
    887           if scroll_command is not None:
    888             self._scroll_output(scroll_command)
    889           return x
    890         else:
    891           command = self._fetch_hyperlink_command(mouse_x, mouse_y)
    892           if command:
    893             self._screen_create_command_textbox()
    894             exit_token = self._dispatch_command(command)
    895             if exit_token is not None:
    896               raise debugger_cli_common.CommandLineExit(exit_token=exit_token)
    897     else:
    898       # Mark the pending command as modified.
    899       self._textbox_pending_command_changed = True
    900       # Invalidate active command history.
    901       self._command_pointer = 0
    902       self._active_command_history = []
    903       return self._KEY_MAP.get(x, x)
    904 
    905   def _screen_getmouse(self):
    906     return curses.getmouse()
    907 
    908   def _redraw_output(self):
    909     if self._curr_unwrapped_output is not None:
    910       self._display_nav_bar()
    911       self._display_main_menu(self._curr_unwrapped_output)
    912       self._display_output(self._curr_unwrapped_output, is_refresh=True)
    913 
    914   def _fetch_hyperlink_command(self, mouse_x, mouse_y):
    915     output_top = self._output_top_row
    916     if self._main_menu_pad:
    917       output_top += 1
    918 
    919     if mouse_y == self._nav_bar_row and self._nav_bar:
    920       # Click was in the nav bar.
    921       return _get_command_from_line_attr_segs(mouse_x,
    922                                               self._nav_bar.font_attr_segs[0])
    923     elif mouse_y == self._output_top_row and self._main_menu_pad:
    924       # Click was in the menu bar.
    925       return _get_command_from_line_attr_segs(mouse_x,
    926                                               self._main_menu.font_attr_segs[0])
    927     else:
    928       absolute_mouse_y = mouse_y + self._output_pad_row - output_top
    929       if absolute_mouse_y in self._curr_wrapped_output.font_attr_segs:
    930         return _get_command_from_line_attr_segs(
    931             mouse_x, self._curr_wrapped_output.font_attr_segs[absolute_mouse_y])
    932 
    933   def _title(self, title, title_color=None):
    934     """Display title.
    935 
    936     Args:
    937       title: (str) The title to display.
    938       title_color: (str) Color of the title, e.g., "yellow".
    939     """
    940 
    941     # Pad input title str with "-" and space characters to make it pretty.
    942     self._title_line = "--- %s " % title
    943     if len(self._title_line) < self._max_x:
    944       self._title_line += "-" * (self._max_x - len(self._title_line))
    945 
    946     self._screen_draw_text_line(
    947         self._title_row, self._title_line, color=title_color)
    948 
    949   def _auto_key_in(self, command, erase_existing=False):
    950     """Automatically key in a command to the command Textbox.
    951 
    952     Args:
    953       command: The command, as a string or None.
    954       erase_existing: (bool) whether existing text (if any) is to be erased
    955           first.
    956     """
    957     if erase_existing:
    958       self._erase_existing_command()
    959 
    960     command = command or ""
    961     for c in command:
    962       self._command_textbox.do_command(ord(c))
    963 
    964   def _erase_existing_command(self):
    965     """Erase existing text in command textpad."""
    966 
    967     existing_len = len(self._command_textbox.gather())
    968     for _ in xrange(existing_len):
    969       self._command_textbox.do_command(self.BACKSPACE_KEY)
    970 
    971   def _screen_draw_text_line(self, row, line, attr=curses.A_NORMAL, color=None):
    972     """Render a line of text on the screen.
    973 
    974     Args:
    975       row: (int) Row index.
    976       line: (str) The line content.
    977       attr: curses font attribute.
    978       color: (str) font foreground color name.
    979 
    980     Raises:
    981       TypeError: If row is not of type int.
    982     """
    983 
    984     if not isinstance(row, int):
    985       raise TypeError("Invalid type in row")
    986 
    987     if len(line) > self._max_x:
    988       line = line[:self._max_x]
    989 
    990     color_pair = (self._default_color_pair if color is None else
    991                   self._color_pairs[color])
    992 
    993     self._addstr(row, 0, line, color_pair | attr)
    994     self._screen_refresh()
    995 
    996   def _screen_new_output_pad(self, rows, cols):
    997     """Generate a new pad on the screen.
    998 
    999     Args:
   1000       rows: (int) Number of rows the pad will have: not limited to screen size.
   1001       cols: (int) Number of columns the pad will have: not limited to screen
   1002         size.
   1003 
   1004     Returns:
   1005       A curses textpad object.
   1006     """
   1007 
   1008     return curses.newpad(rows, cols)
   1009 
   1010   def _screen_display_output(self, output):
   1011     """Actually render text output on the screen.
   1012 
   1013     Wraps the lines according to screen width. Pad lines below according to
   1014     screen height so that the user can scroll the output to a state where
   1015     the last non-empty line is on the top of the screen. Then renders the
   1016     lines on the screen.
   1017 
   1018     Args:
   1019       output: (RichTextLines) text lines to display on the screen. These lines
   1020         may have widths exceeding the screen width. This method will take care
   1021         of the wrapping.
   1022 
   1023     Returns:
   1024       (List of int) A list of line indices, in the wrapped output, where there
   1025         are regex matches.
   1026     """
   1027 
   1028     # Wrap the output lines according to screen width.
   1029     self._curr_wrapped_output, wrapped_line_indices = (
   1030         debugger_cli_common.wrap_rich_text_lines(output, self._max_x - 2))
   1031 
   1032     # Append lines to curr_wrapped_output so that the user can scroll to a
   1033     # state where the last text line is on the top of the output area.
   1034     self._curr_wrapped_output.lines.extend([""] * (self._output_num_rows - 1))
   1035 
   1036     # Limit number of lines displayed to avoid curses overflow problems.
   1037     if self._curr_wrapped_output.num_lines() > self.max_output_lines:
   1038       self._curr_wrapped_output = self._curr_wrapped_output.slice(
   1039           0, self.max_output_lines)
   1040       self._curr_wrapped_output.lines.append("Output cut off at %d lines!" %
   1041                                              self.max_output_lines)
   1042       self._curr_wrapped_output.font_attr_segs[self.max_output_lines] = [
   1043           (0, len(output.lines[-1]), cli_shared.COLOR_MAGENTA)
   1044       ]
   1045 
   1046     self._display_nav_bar()
   1047     self._display_main_menu(self._curr_wrapped_output)
   1048 
   1049     (self._output_pad, self._output_pad_height,
   1050      self._output_pad_width) = self._display_lines(self._curr_wrapped_output,
   1051                                                    self._output_num_rows)
   1052 
   1053     # The indices of lines with regex matches (if any) need to be mapped to
   1054     # indices of wrapped lines.
   1055     return [
   1056         wrapped_line_indices[line]
   1057         for line in self._unwrapped_regex_match_lines
   1058     ]
   1059 
   1060   def _display_output(self, output, is_refresh=False, highlight_regex=None):
   1061     """Display text output in a scrollable text pad.
   1062 
   1063     This method does some preprocessing on the text lines, render them on the
   1064     screen and scroll to the appropriate line. These are done according to regex
   1065     highlighting requests (if any), scroll-to-next-match requests (if any),
   1066     and screen refresh requests (if any).
   1067 
   1068     TODO(cais): Separate these unrelated request to increase clarity and
   1069       maintainability.
   1070 
   1071     Args:
   1072       output: A RichTextLines object that is the screen output text.
   1073       is_refresh: (bool) Is this a refreshing display with existing output.
   1074       highlight_regex: (str) Optional string representing the regex used to
   1075         search and highlight in the current screen output.
   1076     """
   1077 
   1078     if not output:
   1079       return
   1080 
   1081     if highlight_regex:
   1082       try:
   1083         output = debugger_cli_common.regex_find(
   1084             output, highlight_regex, font_attr=self._SEARCH_HIGHLIGHT_FONT_ATTR)
   1085       except ValueError as e:
   1086         self._error_toast(str(e))
   1087         return
   1088 
   1089       if not is_refresh:
   1090         # Perform new regex search on the current output.
   1091         self._unwrapped_regex_match_lines = output.annotations[
   1092             debugger_cli_common.REGEX_MATCH_LINES_KEY]
   1093       else:
   1094         # Continue scrolling down.
   1095         self._output_pad_row += 1
   1096     else:
   1097       self._curr_unwrapped_output = output
   1098       self._unwrapped_regex_match_lines = []
   1099 
   1100     # Display output on the screen.
   1101     wrapped_regex_match_lines = self._screen_display_output(output)
   1102 
   1103     # Now that the text lines are displayed on the screen scroll to the
   1104     # appropriate line according to previous scrolling state and regex search
   1105     # and highlighting state.
   1106 
   1107     if highlight_regex:
   1108       next_match_line = -1
   1109       for match_line in wrapped_regex_match_lines:
   1110         if match_line >= self._output_pad_row:
   1111           next_match_line = match_line
   1112           break
   1113 
   1114       if next_match_line >= 0:
   1115         self._scroll_output(
   1116             _SCROLL_TO_LINE_INDEX, line_index=next_match_line)
   1117       else:
   1118         # Regex search found no match >= current line number. Display message
   1119         # stating as such.
   1120         self._toast("Pattern not found", color=self._ERROR_TOAST_COLOR_PAIR)
   1121     elif is_refresh:
   1122       self._scroll_output(_SCROLL_REFRESH)
   1123     elif debugger_cli_common.INIT_SCROLL_POS_KEY in output.annotations:
   1124       line_index = output.annotations[debugger_cli_common.INIT_SCROLL_POS_KEY]
   1125       self._scroll_output(_SCROLL_TO_LINE_INDEX, line_index=line_index)
   1126     else:
   1127       self._output_pad_row = 0
   1128       self._scroll_output(_SCROLL_HOME)
   1129 
   1130   def _display_lines(self, output, min_num_rows):
   1131     """Display RichTextLines object on screen.
   1132 
   1133     Args:
   1134       output: A RichTextLines object.
   1135       min_num_rows: (int) Minimum number of output rows.
   1136 
   1137     Returns:
   1138       1) The text pad object used to display the main text body.
   1139       2) (int) number of rows of the text pad, which may exceed screen size.
   1140       3) (int) number of columns of the text pad.
   1141 
   1142     Raises:
   1143       ValueError: If input argument "output" is invalid.
   1144     """
   1145 
   1146     if not isinstance(output, debugger_cli_common.RichTextLines):
   1147       raise ValueError(
   1148           "Output is required to be an instance of RichTextLines, but is not.")
   1149 
   1150     self._screen_refresh()
   1151 
   1152     # Number of rows the output area will have.
   1153     rows = max(min_num_rows, len(output.lines))
   1154 
   1155     # Size of the output pad, which may exceed screen size and require
   1156     # scrolling.
   1157     cols = self._max_x - 2
   1158 
   1159     # Create new output pad.
   1160     pad = self._screen_new_output_pad(rows, cols)
   1161 
   1162     for i in xrange(len(output.lines)):
   1163       if i in output.font_attr_segs:
   1164         self._screen_add_line_to_output_pad(
   1165             pad, i, output.lines[i], color_segments=output.font_attr_segs[i])
   1166       else:
   1167         self._screen_add_line_to_output_pad(pad, i, output.lines[i])
   1168 
   1169     return pad, rows, cols
   1170 
   1171   def _display_nav_bar(self):
   1172     nav_bar_width = self._max_x - 2
   1173     self._nav_bar_pad = self._screen_new_output_pad(1, nav_bar_width)
   1174     self._nav_bar = self._nav_history.render(
   1175         nav_bar_width,
   1176         self._NAVIGATION_BACK_COMMAND,
   1177         self._NAVIGATION_FORWARD_COMMAND)
   1178     self._screen_add_line_to_output_pad(
   1179         self._nav_bar_pad, 0, self._nav_bar.lines[0][:nav_bar_width - 1],
   1180         color_segments=(self._nav_bar.font_attr_segs[0]
   1181                         if 0 in self._nav_bar.font_attr_segs else None))
   1182 
   1183   def _display_main_menu(self, output):
   1184     """Display main menu associated with screen output, if the menu exists.
   1185 
   1186     Args:
   1187       output: (debugger_cli_common.RichTextLines) The RichTextLines output from
   1188         the annotations field of which the menu will be extracted and used (if
   1189         the menu exists).
   1190     """
   1191 
   1192     if debugger_cli_common.MAIN_MENU_KEY in output.annotations:
   1193       self._main_menu = output.annotations[
   1194           debugger_cli_common.MAIN_MENU_KEY].format_as_single_line(
   1195               prefix="| ", divider=" | ", enabled_item_attrs=["underline"])
   1196 
   1197       self._main_menu_pad = self._screen_new_output_pad(1, self._max_x - 2)
   1198 
   1199       # The unwrapped menu line may exceed screen width, in which case it needs
   1200       # to be cut off.
   1201       wrapped_menu, _ = debugger_cli_common.wrap_rich_text_lines(
   1202           self._main_menu, self._max_x - 3)
   1203       self._screen_add_line_to_output_pad(
   1204           self._main_menu_pad,
   1205           0,
   1206           wrapped_menu.lines[0],
   1207           color_segments=(wrapped_menu.font_attr_segs[0]
   1208                           if 0 in wrapped_menu.font_attr_segs else None))
   1209     else:
   1210       self._main_menu = None
   1211       self._main_menu_pad = None
   1212 
   1213   def _pad_line_end_with_whitespace(self, pad, row, line_end_x):
   1214     """Pad the whitespace at the end of a line with the default color pair.
   1215 
   1216     Prevents spurious color pairs from appearing at the end of the lines in
   1217     certain text terimnals.
   1218 
   1219     Args:
   1220       pad: The curses pad object to operate on.
   1221       row: (`int`) row index.
   1222       line_end_x: (`int`) column index of the end of the line (beginning of
   1223         the whitespace).
   1224     """
   1225     if line_end_x < self._max_x - 2:
   1226       pad.addstr(row, line_end_x, " " * (self._max_x - 3 - line_end_x),
   1227                  self._default_color_pair)
   1228 
   1229   def _screen_add_line_to_output_pad(self, pad, row, txt, color_segments=None):
   1230     """Render a line in a text pad.
   1231 
   1232     Assumes: segments in color_segments are sorted in ascending order of the
   1233     beginning index.
   1234     Note: Gaps between the segments are allowed and will be fixed in with a
   1235     default color.
   1236 
   1237     Args:
   1238       pad: The text pad to render the line in.
   1239       row: Row index, as an int.
   1240       txt: The text to be displayed on the specified row, as a str.
   1241       color_segments: A list of 3-tuples. Each tuple represents the beginning
   1242         and the end of a color segment, in the form of a right-open interval:
   1243         [start, end). The last element of the tuple is a color string, e.g.,
   1244         "red".
   1245 
   1246     Raisee:
   1247       TypeError: If color_segments is not of type list.
   1248     """
   1249 
   1250     if not color_segments:
   1251       pad.addstr(row, 0, txt, self._default_color_pair)
   1252       self._pad_line_end_with_whitespace(pad, row, len(txt))
   1253       return
   1254 
   1255     if not isinstance(color_segments, list):
   1256       raise TypeError("Input color_segments needs to be a list, but is not.")
   1257 
   1258     all_segments = []
   1259     all_color_pairs = []
   1260 
   1261     # Process the beginning.
   1262     if color_segments[0][0] == 0:
   1263       pass
   1264     else:
   1265       all_segments.append((0, color_segments[0][0]))
   1266       all_color_pairs.append(self._default_color_pair)
   1267 
   1268     for (curr_start, curr_end, curr_attrs), (next_start, _, _) in zip(
   1269         color_segments, color_segments[1:] + [(len(txt), None, None)]):
   1270       all_segments.append((curr_start, curr_end))
   1271 
   1272       if not isinstance(curr_attrs, list):
   1273         curr_attrs = [curr_attrs]
   1274 
   1275       curses_attr = curses.A_NORMAL
   1276       for attr in curr_attrs:
   1277         if (self._mouse_enabled and
   1278             isinstance(attr, debugger_cli_common.MenuItem)):
   1279           curses_attr |= curses.A_UNDERLINE
   1280         else:
   1281           curses_attr |= self._color_pairs.get(attr, self._default_color_pair)
   1282       all_color_pairs.append(curses_attr)
   1283 
   1284       if curr_end < next_start:
   1285         # Fill in the gap with the default color.
   1286         all_segments.append((curr_end, next_start))
   1287         all_color_pairs.append(self._default_color_pair)
   1288 
   1289     # Finally, draw all the segments.
   1290     for segment, color_pair in zip(all_segments, all_color_pairs):
   1291       if segment[1] < self._max_x:
   1292         pad.addstr(row, segment[0], txt[segment[0]:segment[1]], color_pair)
   1293     if all_segments:
   1294       self._pad_line_end_with_whitespace(pad, row, all_segments[-1][1])
   1295 
   1296   def _screen_scroll_output_pad(self, pad, viewport_top, viewport_left,
   1297                                 screen_location_top, screen_location_left,
   1298                                 screen_location_bottom, screen_location_right):
   1299     self._refresh_pad(pad, viewport_top, viewport_left, screen_location_top,
   1300                       screen_location_left, screen_location_bottom,
   1301                       screen_location_right)
   1302     self._scroll_bar = ScrollBar(
   1303         self._max_x - 2,
   1304         3,
   1305         self._max_x - 1,
   1306         self._output_num_rows + 1,
   1307         self._output_pad_row,
   1308         self._output_pad_height - self._output_pad_screen_height)
   1309 
   1310     (scroll_pad, _, _) = self._display_lines(
   1311         self._scroll_bar.layout(), self._output_num_rows - 1)
   1312     self._refresh_pad(scroll_pad, 0, 0, self._output_top_row + 1,
   1313                       self._max_x - 2, self._output_num_rows + 1,
   1314                       self._max_x - 1)
   1315 
   1316   def _scroll_output(self, direction, line_index=None):
   1317     """Scroll the output pad.
   1318 
   1319     Args:
   1320       direction: _SCROLL_REFRESH, _SCROLL_UP, _SCROLL_DOWN, _SCROLL_UP_A_LINE,
   1321         _SCROLL_DOWN_A_LINE, _SCROLL_HOME, _SCROLL_END, _SCROLL_TO_LINE_INDEX
   1322       line_index: (int) Specifies the zero-based line index to scroll to.
   1323         Applicable only if direction is _SCROLL_TO_LINE_INDEX.
   1324 
   1325     Raises:
   1326       ValueError: On invalid scroll direction.
   1327       TypeError: If line_index is not int and direction is
   1328         _SCROLL_TO_LINE_INDEX.
   1329     """
   1330 
   1331     if not self._output_pad:
   1332       # No output pad is present. Do nothing.
   1333       return
   1334 
   1335     if direction == _SCROLL_REFRESH:
   1336       pass
   1337     elif direction == _SCROLL_UP:
   1338       # Scroll up.
   1339       self._output_pad_row -= int(self._output_num_rows / 3)
   1340       if self._output_pad_row < 0:
   1341         self._output_pad_row = 0
   1342     elif direction == _SCROLL_DOWN:
   1343       # Scroll down.
   1344       self._output_pad_row += int(self._output_num_rows / 3)
   1345       if (self._output_pad_row >
   1346           self._output_pad_height - self._output_pad_screen_height - 1):
   1347         self._output_pad_row = (
   1348             self._output_pad_height - self._output_pad_screen_height - 1)
   1349     elif direction == _SCROLL_UP_A_LINE:
   1350       # Scroll up a line
   1351       if self._output_pad_row - 1 >= 0:
   1352         self._output_pad_row -= 1
   1353     elif direction == _SCROLL_DOWN_A_LINE:
   1354       # Scroll down a line
   1355       if self._output_pad_row + 1 < (
   1356           self._output_pad_height - self._output_pad_screen_height):
   1357         self._output_pad_row += 1
   1358     elif direction == _SCROLL_HOME:
   1359       # Scroll to top
   1360       self._output_pad_row = 0
   1361     elif direction == _SCROLL_END:
   1362       # Scroll to bottom
   1363       self._output_pad_row = (
   1364           self._output_pad_height - self._output_pad_screen_height - 1)
   1365     elif direction == _SCROLL_TO_LINE_INDEX:
   1366       if not isinstance(line_index, int):
   1367         raise TypeError("Invalid line_index type (%s) under mode %s" %
   1368                         (type(line_index), _SCROLL_TO_LINE_INDEX))
   1369       self._output_pad_row = line_index
   1370     else:
   1371       raise ValueError("Unsupported scroll mode: %s" % direction)
   1372 
   1373     self._nav_history.update_scroll_position(self._output_pad_row)
   1374 
   1375     # Actually scroll the output pad: refresh with new location.
   1376     output_pad_top = self._output_pad_screen_location.top
   1377     if self._main_menu_pad:
   1378       output_pad_top += 1
   1379     self._screen_scroll_output_pad(self._output_pad, self._output_pad_row, 0,
   1380                                    output_pad_top,
   1381                                    self._output_pad_screen_location.left,
   1382                                    self._output_pad_screen_location.bottom,
   1383                                    self._output_pad_screen_location.right)
   1384     self._screen_render_nav_bar()
   1385     self._screen_render_menu_pad()
   1386 
   1387     self._scroll_info = self._compile_ui_status_summary()
   1388     self._screen_draw_text_line(
   1389         self._output_scroll_row,
   1390         self._scroll_info,
   1391         color=self._STATUS_BAR_COLOR_PAIR)
   1392 
   1393   def _screen_render_nav_bar(self):
   1394     if self._nav_bar_pad:
   1395       self._refresh_pad(self._nav_bar_pad, 0, 0, self._nav_bar_row, 0,
   1396                         self._output_pad_screen_location.top, self._max_x)
   1397 
   1398   def _screen_render_menu_pad(self):
   1399     if self._main_menu_pad:
   1400       self._refresh_pad(
   1401           self._main_menu_pad, 0, 0, self._output_pad_screen_location.top, 0,
   1402           self._output_pad_screen_location.top, self._max_x)
   1403 
   1404   def _compile_ui_status_summary(self):
   1405     """Compile status summary about this Curses UI instance.
   1406 
   1407     The information includes: scroll status and mouse ON/OFF status.
   1408 
   1409     Returns:
   1410       (str) A single text line summarizing the UI status, adapted to the
   1411         current screen width.
   1412     """
   1413 
   1414     info = ""
   1415     if self._output_pad_height > self._output_pad_screen_height + 1:
   1416       # Display information about the scrolling of tall screen output.
   1417       scroll_percentage = 100.0 * (min(
   1418           1.0,
   1419           float(self._output_pad_row) /
   1420           (self._output_pad_height - self._output_pad_screen_height - 1)))
   1421       if self._output_pad_row == 0:
   1422         scroll_directions = " (PgDn)"
   1423       elif self._output_pad_row >= (
   1424           self._output_pad_height - self._output_pad_screen_height - 1):
   1425         scroll_directions = " (PgUp)"
   1426       else:
   1427         scroll_directions = " (PgDn/PgUp)"
   1428 
   1429       info += "--- Scroll%s: %.2f%% " % (scroll_directions, scroll_percentage)
   1430 
   1431     self._output_array_pointer_indices = self._show_array_indices()
   1432 
   1433     # Add array indices information to scroll message.
   1434     if self._output_array_pointer_indices:
   1435       if self._output_array_pointer_indices[0]:
   1436         info += self._format_indices(self._output_array_pointer_indices[0])
   1437       info += "-"
   1438       if self._output_array_pointer_indices[-1]:
   1439         info += self._format_indices(self._output_array_pointer_indices[-1])
   1440       info += " "
   1441 
   1442     # Add mouse mode information.
   1443     mouse_mode_str = "Mouse: "
   1444     mouse_mode_str += "ON" if self._mouse_enabled else "OFF"
   1445 
   1446     if len(info) + len(mouse_mode_str) + 5 < self._max_x:
   1447       info += "-" * (self._max_x - len(info) - len(mouse_mode_str) - 4)
   1448       info += " "
   1449       info += mouse_mode_str
   1450       info += " ---"
   1451     else:
   1452       info += "-" * (self._max_x - len(info))
   1453 
   1454     return info
   1455 
   1456   def _format_indices(self, indices):
   1457     # Remove the spaces to make it compact.
   1458     return repr(indices).replace(" ", "")
   1459 
   1460   def _show_array_indices(self):
   1461     """Show array indices for the lines at the top and bottom of the output.
   1462 
   1463     For the top line and bottom line of the output display area, show the
   1464     element indices of the array being displayed.
   1465 
   1466     Returns:
   1467       If either the top of the bottom row has any matching array indices,
   1468       a dict from line index (0 being the top of the display area, -1
   1469       being the bottom of the display area) to array element indices. For
   1470       example:
   1471         {0: [0, 0], -1: [10, 0]}
   1472       Otherwise, None.
   1473     """
   1474 
   1475     indices_top = self._show_array_index_at_line(0)
   1476 
   1477     output_top = self._output_top_row
   1478     if self._main_menu_pad:
   1479       output_top += 1
   1480     bottom_line_index = (
   1481         self._output_pad_screen_location.bottom - output_top - 1)
   1482     indices_bottom = self._show_array_index_at_line(bottom_line_index)
   1483 
   1484     if indices_top or indices_bottom:
   1485       return {0: indices_top, -1: indices_bottom}
   1486     else:
   1487       return None
   1488 
   1489   def _show_array_index_at_line(self, line_index):
   1490     """Show array indices for the specified line in the display area.
   1491 
   1492     Uses the line number to array indices map in the annotations field of the
   1493     RichTextLines object being displayed.
   1494     If the displayed RichTextLines object does not contain such a mapping,
   1495     will do nothing.
   1496 
   1497     Args:
   1498       line_index: (int) 0-based line index from the top of the display area.
   1499         For example,if line_index == 0, this method will display the array
   1500         indices for the line currently at the top of the display area.
   1501 
   1502     Returns:
   1503       (list) The array indices at the specified line, if available. None, if
   1504         not available.
   1505     """
   1506 
   1507     # Examine whether the index information is available for the specified line
   1508     # number.
   1509     pointer = self._output_pad_row + line_index
   1510     if (pointer in self._curr_wrapped_output.annotations and
   1511         "i0" in self._curr_wrapped_output.annotations[pointer]):
   1512       indices = self._curr_wrapped_output.annotations[pointer]["i0"]
   1513 
   1514       array_indices_str = self._format_indices(indices)
   1515       array_indices_info = "@" + array_indices_str
   1516 
   1517       # TODO(cais): Determine line_index properly given menu pad status.
   1518       #   Test coverage?
   1519       output_top = self._output_top_row
   1520       if self._main_menu_pad:
   1521         output_top += 1
   1522 
   1523       self._toast(
   1524           array_indices_info,
   1525           color=self._ARRAY_INDICES_COLOR_PAIR,
   1526           line_index=output_top + line_index)
   1527 
   1528       return indices
   1529     else:
   1530       return None
   1531 
   1532   def _tab_complete(self, command_str):
   1533     """Perform tab completion.
   1534 
   1535     Obtains tab completion candidates.
   1536     If there are no candidates, return command_str and take no other actions.
   1537     If there are candidates, display the candidates on screen and return
   1538     command_str + (common prefix of the candidates).
   1539 
   1540     Args:
   1541       command_str: (str) The str in the command input textbox when Tab key is
   1542         hit.
   1543 
   1544     Returns:
   1545       (str) Completed string. Could be the same as command_str if no completion
   1546       candidate is available. If candidate(s) are available, return command_str
   1547       appended by the common prefix of the candidates.
   1548     """
   1549 
   1550     context, prefix, except_last_word = self._analyze_tab_complete_input(
   1551         command_str)
   1552     candidates, common_prefix = self._tab_completion_registry.get_completions(
   1553         context, prefix)
   1554 
   1555     if candidates and len(candidates) > 1:
   1556       self._display_candidates(candidates)
   1557     else:
   1558       # In the case of len(candidates) == 1, the single completion will be
   1559       # entered to the textbox automatically. So there is no need to show any
   1560       # candidates.
   1561       self._display_candidates([])
   1562 
   1563     if common_prefix:
   1564       # Common prefix is not None and non-empty. The completed string will
   1565       # incorporate the common prefix.
   1566       return except_last_word + common_prefix
   1567     else:
   1568       return except_last_word + prefix
   1569 
   1570   def _display_candidates(self, candidates):
   1571     """Show candidates (e.g., tab-completion candidates) on multiple lines.
   1572 
   1573     Args:
   1574       candidates: (list of str) candidates.
   1575     """
   1576 
   1577     if self._curr_unwrapped_output:
   1578       # Force refresh screen output.
   1579       self._scroll_output(_SCROLL_REFRESH)
   1580 
   1581     if not candidates:
   1582       return
   1583 
   1584     candidates_prefix = "Candidates: "
   1585     candidates_line = candidates_prefix + " ".join(candidates)
   1586     candidates_output = debugger_cli_common.RichTextLines(
   1587         candidates_line,
   1588         font_attr_segs={
   1589             0: [(len(candidates_prefix), len(candidates_line), "yellow")]
   1590         })
   1591 
   1592     candidates_output, _ = debugger_cli_common.wrap_rich_text_lines(
   1593         candidates_output, self._max_x - 3)
   1594 
   1595     # Calculate how many lines the candidate text should occupy. Limit it to
   1596     # a maximum value.
   1597     candidates_num_rows = min(
   1598         len(candidates_output.lines), self._candidates_max_lines)
   1599     self._candidates_top_row = (
   1600         self._candidates_bottom_row - candidates_num_rows + 1)
   1601 
   1602     # Render the candidate text on screen.
   1603     pad, _, _ = self._display_lines(candidates_output, 0)
   1604     self._screen_scroll_output_pad(
   1605         pad, 0, 0, self._candidates_top_row, 0,
   1606         self._candidates_top_row + candidates_num_rows - 1, self._max_x - 2)
   1607 
   1608   def _toast(self, message, color=None, line_index=None):
   1609     """Display a one-line message on the screen.
   1610 
   1611     By default, the toast is displayed in the line right above the scroll bar.
   1612     But the line location can be overridden with the line_index arg.
   1613 
   1614     Args:
   1615       message: (str) the message to display.
   1616       color: (str) optional color attribute for the message.
   1617       line_index: (int) line index.
   1618     """
   1619 
   1620     pad, _, _ = self._display_lines(
   1621         debugger_cli_common.RichTextLines(
   1622             message,
   1623             font_attr_segs={
   1624                 0: [(0, len(message), color or cli_shared.COLOR_WHITE)]}),
   1625         0)
   1626 
   1627     right_end = min(len(message), self._max_x - 2)
   1628 
   1629     if line_index is None:
   1630       line_index = self._output_scroll_row - 1
   1631     self._screen_scroll_output_pad(pad, 0, 0, line_index, 0, line_index,
   1632                                    right_end)
   1633 
   1634   def _error_toast(self, message):
   1635     """Display a one-line error message on screen.
   1636 
   1637     Args:
   1638       message: The error message, without the preceding "ERROR: " substring.
   1639     """
   1640 
   1641     self._toast(
   1642         self.ERROR_MESSAGE_PREFIX + message, color=self._ERROR_TOAST_COLOR_PAIR)
   1643 
   1644   def _info_toast(self, message):
   1645     """Display a one-line informational message on screen.
   1646 
   1647     Args:
   1648       message: The informational message.
   1649     """
   1650 
   1651     self._toast(
   1652         self.INFO_MESSAGE_PREFIX + message, color=self._INFO_TOAST_COLOR_PAIR)
   1653 
   1654   def _interrupt_handler(self, signal_num, frame):
   1655     del signal_num  # Unused.
   1656     del frame  # Unused.
   1657 
   1658     if self._on_ui_exit:
   1659       self._on_ui_exit()
   1660 
   1661     self._screen_terminate()
   1662     print("\ntfdbg: caught SIGINT; calling sys.exit(1).", file=sys.stderr)
   1663     sys.exit(1)
   1664 
   1665   def _mouse_mode_command_handler(self, args, screen_info=None):
   1666     """Handler for the command prefix 'mouse'.
   1667 
   1668     Args:
   1669       args: (list of str) Arguments to the command prefix 'mouse'.
   1670       screen_info: (dict) Information about the screen, unused by this handler.
   1671 
   1672     Returns:
   1673       None, as this command handler does not generate any screen outputs other
   1674         than toasts.
   1675     """
   1676 
   1677     del screen_info
   1678 
   1679     if not args or len(args) == 1:
   1680       if args:
   1681         if args[0].lower() == "on":
   1682           enabled = True
   1683         elif args[0].lower() == "off":
   1684           enabled = False
   1685         else:
   1686           self._error_toast("Invalid mouse mode: %s" % args[0])
   1687           return None
   1688 
   1689         self._set_mouse_enabled(enabled)
   1690 
   1691       mode_str = "on" if self._mouse_enabled else "off"
   1692       self._info_toast("Mouse mode: %s" % mode_str)
   1693     else:
   1694       self._error_toast("mouse_mode: syntax error")
   1695 
   1696     return None
   1697 
   1698   def _set_mouse_enabled(self, enabled):
   1699     if self._mouse_enabled != enabled:
   1700       self._mouse_enabled = enabled
   1701       self._screen_set_mousemask()
   1702       self._redraw_output()
   1703 
   1704   def _screen_set_mousemask(self):
   1705     if self._mouse_enabled:
   1706       curses.mousemask(curses.BUTTON1_RELEASED | curses.BUTTON1_PRESSED)
   1707     else:
   1708       curses.mousemask(0)
   1709