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