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