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