• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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)."""
16from __future__ import absolute_import
17from __future__ import division
18from __future__ import print_function
19
20import collections
21import curses
22from curses import textpad
23import os
24import signal
25import sys
26import threading
27
28from six.moves import xrange  # pylint: disable=redefined-builtin
29
30from tensorflow.python.debug.cli import base_ui
31from tensorflow.python.debug.cli import cli_shared
32from tensorflow.python.debug.cli import command_parser
33from tensorflow.python.debug.cli import curses_widgets
34from tensorflow.python.debug.cli import debugger_cli_common
35from 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
51def _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
71class 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
209class 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