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