1#!/usr/bin/env python 2# Copyright 2022 The Pigweed Authors 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not 5# use this file except in compliance with the License. You may obtain a copy of 6# the License at 7# 8# https://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations under 14# the License. 15""" Prompt toolkit application for pw watch. """ 16 17import asyncio 18import functools 19import logging 20import os 21import re 22import time 23from typing import Callable, Dict, Iterable, List, NoReturn, Optional 24 25from prompt_toolkit.application import Application 26from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard 27from prompt_toolkit.filters import Condition 28from prompt_toolkit.history import ( 29 InMemoryHistory, 30 History, 31 ThreadedHistory, 32) 33from prompt_toolkit.key_binding import ( 34 KeyBindings, 35 KeyBindingsBase, 36 merge_key_bindings, 37) 38from prompt_toolkit.layout import ( 39 DynamicContainer, 40 Float, 41 FloatContainer, 42 FormattedTextControl, 43 HSplit, 44 Layout, 45 Window, 46) 47from prompt_toolkit.layout.controls import BufferControl 48from prompt_toolkit.styles import ( 49 ConditionalStyleTransformation, 50 DynamicStyle, 51 SwapLightAndDarkStyleTransformation, 52 merge_style_transformations, 53 merge_styles, 54 style_from_pygments_cls, 55) 56from prompt_toolkit.formatted_text import StyleAndTextTuples 57from prompt_toolkit.lexers import PygmentsLexer 58from pygments.lexers.markup import MarkdownLexer # type: ignore 59 60from pw_console.console_app import get_default_colordepth 61from pw_console.get_pw_console_app import PW_CONSOLE_APP_CONTEXTVAR 62from pw_console.help_window import HelpWindow 63from pw_console.key_bindings import DEFAULT_KEY_BINDINGS 64from pw_console.log_pane import LogPane 65from pw_console.plugin_mixin import PluginMixin 66import pw_console.python_logging 67from pw_console.quit_dialog import QuitDialog 68from pw_console.style import generate_styles, get_theme_colors 69from pw_console.pigweed_code_style import PigweedCodeStyle 70from pw_console.widgets import ( 71 FloatingWindowPane, 72 ToolbarButton, 73 WindowPaneToolbar, 74 create_border, 75 mouse_handlers, 76 to_checkbox, 77) 78from pw_console.window_list import DisplayMode 79from pw_console.window_manager import WindowManager 80 81from pw_build.project_builder_prefs import ProjectBuilderPrefs 82from pw_build.project_builder_context import get_project_builder_context 83 84 85_LOG = logging.getLogger('pw_build.watch') 86 87BUILDER_CONTEXT = get_project_builder_context() 88 89_HELP_TEXT = """ 90Mouse Keys 91========== 92 93- Click on a line in the bottom progress bar to switch to that tab. 94- Click on any tab, or button to activate. 95- Scroll wheel in the the log windows moves back through the history. 96 97 98Global Keys 99=========== 100 101Quit with confirmation dialog. -------------------- Ctrl-D 102Quit without confirmation. ------------------------ Ctrl-X Ctrl-C 103Toggle user guide window. ------------------------- F1 104Trigger a rebuild. -------------------------------- Enter 105 106 107Window Management Keys 108====================== 109 110Switch focus to the next window pane or tab. ------ Ctrl-Alt-N 111Switch focus to the previous window pane or tab. -- Ctrl-Alt-P 112Move window pane left. ---------------------------- Ctrl-Alt-Left 113Move window pane right. --------------------------- Ctrl-Alt-Right 114Move window pane down. ---------------------------- Ctrl-Alt-Down 115Move window pane up. ------------------------------ Ctrl-Alt-Up 116Balance all window sizes. ------------------------- Ctrl-U 117 118 119Bottom Toolbar Controls 120======================= 121 122Rebuild Enter --------------- Click or press Enter to trigger a rebuild. 123[x] Auto Rebuild ------------ Click to globaly enable or disable automatic 124 rebuilding when files change. 125Help F1 --------------------- Click or press F1 to open this help window. 126Quit Ctrl-d ----------------- Click or press Ctrl-d to quit pw_watch. 127Next Tab Ctrl-Alt-n --------- Switch to the next log tab. 128Previous Tab Ctrl-Alt-p ----- Switch to the previous log tab. 129 130 131Build Status Bar 132================ 133 134The build status bar shows the current status of all build directories outlined 135in a colored frame. 136 137 ┏━━ BUILDING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 138 ┃ [✓] out_directory Building Last line of standard out. ┃ 139 ┃ [✓] out_dir2 Waiting Last line of standard out. ┃ 140 ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 141 142Each checkbox on the far left controls whether that directory is built when 143files change and manual builds are run. 144 145 146Copying Text 147============ 148 149- Click drag will select whole lines in the log windows. 150- `Ctrl-c` will copy selected lines to your system clipboard. 151 152If running over SSH you will need to use your terminal's built in text 153selection. 154 155Linux 156----- 157 158- Holding `Shift` and dragging the mouse in most terminals. 159 160Mac 161--- 162 163- Apple Terminal: 164 165 Hold `Fn` and drag the mouse 166 167- iTerm2: 168 169 Hold `Cmd+Option` and drag the mouse 170 171Windows 172------- 173 174- Git CMD (included in `Git for Windows) 175 176 1. Click on the Git window icon in the upper left of the title bar 177 2. Click `Edit` then `Mark` 178 3. Drag the mouse to select text and press Enter to copy. 179 180- Windows Terminal 181 182 1. Hold `Shift` and drag the mouse to select text 183 2. Press `Ctrl-Shift-C` to copy. 184 185""" 186 187 188class WatchAppPrefs(ProjectBuilderPrefs): 189 """Add pw_console specific prefs standard ProjectBuilderPrefs.""" 190 191 def __init__(self, *args, **kwargs) -> None: 192 super().__init__(*args, **kwargs) 193 194 self.registered_commands = DEFAULT_KEY_BINDINGS 195 self.registered_commands.update(self.user_key_bindings) 196 197 new_config_settings = { 198 'key_bindings': DEFAULT_KEY_BINDINGS, 199 'show_python_logger': True, 200 } 201 self.default_config.update(new_config_settings) 202 self._update_config(new_config_settings) 203 204 # Required pw_console preferences for key bindings and themes 205 @property 206 def user_key_bindings(self) -> Dict[str, List[str]]: 207 return self._config.get('key_bindings', {}) 208 209 @property 210 def ui_theme(self) -> str: 211 return self._config.get('ui_theme', '') 212 213 @ui_theme.setter 214 def ui_theme(self, new_ui_theme: str) -> None: 215 self._config['ui_theme'] = new_ui_theme 216 217 @property 218 def theme_colors(self): 219 return get_theme_colors(self.ui_theme) 220 221 @property 222 def swap_light_and_dark(self) -> bool: 223 return self._config.get('swap_light_and_dark', False) 224 225 def get_function_keys(self, name: str) -> List: 226 """Return the keys for the named function.""" 227 try: 228 return self.registered_commands[name] 229 except KeyError as error: 230 raise KeyError('Unbound key function: {}'.format(name)) from error 231 232 def register_named_key_function( 233 self, name: str, default_bindings: List[str] 234 ) -> None: 235 self.registered_commands[name] = default_bindings 236 237 def register_keybinding( 238 self, name: str, key_bindings: KeyBindings, **kwargs 239 ) -> Callable: 240 """Apply registered keys for the given named function.""" 241 242 def decorator(handler: Callable) -> Callable: 243 "`handler` is a callable or Binding." 244 for keys in self.get_function_keys(name): 245 key_bindings.add(*keys.split(' '), **kwargs)(handler) 246 return handler 247 248 return decorator 249 250 # Required pw_console preferences for using a log window pane. 251 @property 252 def spaces_between_columns(self) -> int: 253 return 2 254 255 @property 256 def window_column_split_method(self) -> str: 257 return 'vertical' 258 259 @property 260 def hide_date_from_log_time(self) -> bool: 261 return True 262 263 @property 264 def column_order(self) -> list: 265 return [] 266 267 def column_style( # pylint: disable=no-self-use 268 self, 269 _column_name: str, 270 _column_value: str, 271 default='', 272 ) -> str: 273 return default 274 275 @property 276 def show_python_file(self) -> bool: 277 return self._config.get('show_python_file', False) 278 279 @property 280 def show_source_file(self) -> bool: 281 return self._config.get('show_source_file', False) 282 283 @property 284 def show_python_logger(self) -> bool: 285 return self._config.get('show_python_logger', False) 286 287 288class WatchWindowManager(WindowManager): 289 def update_root_container_body(self): 290 self.application.window_manager_container = self.create_root_container() 291 292 293class WatchApp(PluginMixin): 294 """Pigweed Watch main window application.""" 295 296 # pylint: disable=too-many-instance-attributes 297 298 NINJA_FAILURE_TEXT = '\033[31mFAILED: ' 299 300 NINJA_BUILD_STEP = re.compile( 301 r"^\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\] (?P<action>.*)$" 302 ) 303 304 def __init__( 305 self, 306 event_handler, 307 prefs: WatchAppPrefs, 308 ): 309 self.event_handler = event_handler 310 311 self.color_depth = get_default_colordepth() 312 313 # Necessary for some of pw_console's window manager features to work 314 # such as mouse drag resizing. 315 PW_CONSOLE_APP_CONTEXTVAR.set(self) # type: ignore 316 317 self.prefs = prefs 318 319 self.quit_dialog = QuitDialog(self, self.exit) # type: ignore 320 321 self.search_history: History = ThreadedHistory(InMemoryHistory()) 322 323 self.window_manager = WatchWindowManager(self) 324 325 self._build_error_count = 0 326 self._errors_in_output = False 327 328 self.log_ui_update_frequency = 0.1 # 10 FPS 329 self._last_ui_update_time = time.time() 330 331 self.recipe_name_to_log_pane: Dict[str, LogPane] = {} 332 self.recipe_index_to_log_pane: Dict[int, LogPane] = {} 333 334 debug_logging = ( 335 event_handler.project_builder.default_log_level == logging.DEBUG 336 ) 337 level_name = 'DEBUG' if debug_logging else 'INFO' 338 339 no_propagation_loggers = [] 340 341 if event_handler.separate_logfiles: 342 pane_index = len(event_handler.project_builder.build_recipes) - 1 343 for recipe in reversed(event_handler.project_builder.build_recipes): 344 log_pane = self.add_build_log_pane( 345 recipe.display_name, 346 loggers=[recipe.log], 347 level_name=level_name, 348 ) 349 if recipe.log.propagate is False: 350 no_propagation_loggers.append(recipe.log) 351 352 self.recipe_name_to_log_pane[recipe.display_name] = log_pane 353 self.recipe_index_to_log_pane[pane_index] = log_pane 354 pane_index -= 1 355 356 pw_console.python_logging.setup_python_logging( 357 loggers_with_no_propagation=no_propagation_loggers 358 ) 359 360 self.root_log_pane = self.add_build_log_pane( 361 'Root Log', 362 loggers=[ 363 logging.getLogger('pw_build'), 364 ], 365 level_name=level_name, 366 ) 367 368 self.window_manager.window_lists[0].display_mode = DisplayMode.TABBED 369 370 self.window_manager_container = ( 371 self.window_manager.create_root_container() 372 ) 373 374 self.status_bar_border_style = 'class:command-runner-border' 375 376 self.status_bar_control = FormattedTextControl(self.get_status_bar_text) 377 378 self.status_bar_container = create_border( 379 HSplit( 380 [ 381 # Result Toolbar. 382 Window( 383 content=self.status_bar_control, 384 height=len(self.event_handler.project_builder), 385 wrap_lines=False, 386 style='class:pane_active', 387 ), 388 ] 389 ), 390 content_height=len(self.event_handler.project_builder), 391 title=BUILDER_CONTEXT.get_title_bar_text, 392 border_style=(BUILDER_CONTEXT.get_title_style), 393 base_style='class:pane_active', 394 left_margin_columns=1, 395 right_margin_columns=1, 396 ) 397 398 self.floating_window_plugins: List[FloatingWindowPane] = [] 399 400 self.user_guide_window = HelpWindow( 401 self, # type: ignore 402 title='Pigweed Watch', 403 disable_ctrl_c=True, 404 ) 405 self.user_guide_window.set_help_text( 406 _HELP_TEXT, lexer=PygmentsLexer(MarkdownLexer) 407 ) 408 409 self.help_toolbar = WindowPaneToolbar( 410 title='Pigweed Watch', 411 include_resize_handle=False, 412 focus_action_callable=self.switch_to_root_log, 413 click_to_focus_text='', 414 ) 415 self.help_toolbar.add_button( 416 ToolbarButton('Enter', 'Rebuild', self.run_build) 417 ) 418 self.help_toolbar.add_button( 419 ToolbarButton( 420 description='Auto Rebuild', 421 mouse_handler=self.toggle_restart_on_filechange, 422 is_checkbox=True, 423 checked=lambda: self.restart_on_changes, 424 ) 425 ) 426 self.help_toolbar.add_button( 427 ToolbarButton('F1', 'Help', self.user_guide_window.toggle_display) 428 ) 429 self.help_toolbar.add_button(ToolbarButton('Ctrl-d', 'Quit', self.exit)) 430 self.help_toolbar.add_button( 431 ToolbarButton( 432 'Ctrl-Alt-n', 'Next Tab', self.window_manager.focus_next_pane 433 ) 434 ) 435 self.help_toolbar.add_button( 436 ToolbarButton( 437 'Ctrl-Alt-p', 438 'Previous Tab', 439 self.window_manager.focus_previous_pane, 440 ) 441 ) 442 443 self.root_container = FloatContainer( 444 HSplit( 445 [ 446 # Window pane content: 447 DynamicContainer(lambda: self.window_manager_container), 448 self.status_bar_container, 449 self.help_toolbar, 450 ] 451 ), 452 floats=[ 453 Float( 454 content=self.user_guide_window, 455 top=2, 456 left=4, 457 bottom=4, 458 width=self.user_guide_window.content_width, 459 ), 460 Float( 461 content=self.quit_dialog, 462 top=2, 463 left=2, 464 ), 465 ], 466 ) 467 468 key_bindings = KeyBindings() 469 470 @key_bindings.add('enter', filter=self.input_box_not_focused()) 471 def _run_build(_event): 472 "Rebuild." 473 self.run_build() 474 475 register = self.prefs.register_keybinding 476 477 @register('global.exit-no-confirmation', key_bindings) 478 def _quit_no_confirm(_event): 479 """Quit without confirmation.""" 480 _LOG.info('Got quit signal; exiting...') 481 self.exit(0) 482 483 @register('global.exit-with-confirmation', key_bindings) 484 def _quit_with_confirm(_event): 485 """Quit with confirmation dialog.""" 486 self.quit_dialog.open_dialog() 487 488 @register( 489 'global.open-user-guide', 490 key_bindings, 491 filter=Condition(lambda: not self.modal_window_is_open()), 492 ) 493 def _show_help(_event): 494 """Toggle user guide window.""" 495 self.user_guide_window.toggle_display() 496 497 self.key_bindings = merge_key_bindings( 498 [ 499 self.window_manager.key_bindings, 500 key_bindings, 501 ] 502 ) 503 504 self.current_theme = generate_styles(self.prefs.ui_theme) 505 506 self.style_transformation = merge_style_transformations( 507 [ 508 ConditionalStyleTransformation( 509 SwapLightAndDarkStyleTransformation(), 510 filter=Condition(lambda: self.prefs.swap_light_and_dark), 511 ), 512 ] 513 ) 514 515 self.code_theme = style_from_pygments_cls(PigweedCodeStyle) 516 517 self.layout = Layout( 518 self.root_container, 519 focused_element=self.root_log_pane, 520 ) 521 522 self.application: Application = Application( 523 layout=self.layout, 524 key_bindings=self.key_bindings, 525 mouse_support=True, 526 color_depth=self.color_depth, 527 clipboard=PyperclipClipboard(), 528 style=DynamicStyle( 529 lambda: merge_styles( 530 [ 531 self.current_theme, 532 self.code_theme, 533 ] 534 ) 535 ), 536 style_transformation=self.style_transformation, 537 full_screen=True, 538 ) 539 540 self.plugin_init( 541 plugin_callback=self.check_build_status, 542 plugin_callback_frequency=0.5, 543 plugin_logger_name='pw_watch_stdout_checker', 544 ) 545 546 def add_build_log_pane( 547 self, title: str, loggers: List[logging.Logger], level_name: str 548 ) -> LogPane: 549 """Setup a new build log pane.""" 550 new_log_pane = LogPane(application=self, pane_title=title) 551 for logger in loggers: 552 new_log_pane.add_log_handler(logger, level_name=level_name) 553 554 # Set python log format to just the message itself. 555 new_log_pane.log_view.log_store.formatter = logging.Formatter( 556 '%(message)s' 557 ) 558 559 new_log_pane.table_view = False 560 561 # Disable line wrapping for improved error visibility. 562 if new_log_pane.wrap_lines: 563 new_log_pane.toggle_wrap_lines() 564 565 # Blank right side toolbar text 566 new_log_pane._pane_subtitle = ' ' # pylint: disable=protected-access 567 568 # Make tab and shift-tab search for next and previous error 569 next_error_bindings = KeyBindings() 570 571 @next_error_bindings.add('s-tab') 572 def _previous_error(_event): 573 self.jump_to_error(backwards=True) 574 575 @next_error_bindings.add('tab') 576 def _next_error(_event): 577 self.jump_to_error() 578 579 existing_log_bindings: Optional[ 580 KeyBindingsBase 581 ] = new_log_pane.log_content_control.key_bindings 582 583 key_binding_list: List[KeyBindingsBase] = [] 584 if existing_log_bindings: 585 key_binding_list.append(existing_log_bindings) 586 key_binding_list.append(next_error_bindings) 587 new_log_pane.log_content_control.key_bindings = merge_key_bindings( 588 key_binding_list 589 ) 590 591 # Only show a few buttons in the log pane toolbars. 592 new_buttons = [] 593 for button in new_log_pane.bottom_toolbar.buttons: 594 if button.description in [ 595 'Search', 596 'Save', 597 'Follow', 598 'Wrap', 599 'Clear', 600 ]: 601 new_buttons.append(button) 602 new_log_pane.bottom_toolbar.buttons = new_buttons 603 604 self.window_manager.add_pane(new_log_pane) 605 return new_log_pane 606 607 def logs_redraw(self): 608 emit_time = time.time() 609 # Has enough time passed since last UI redraw due to new logs? 610 if emit_time > self._last_ui_update_time + self.log_ui_update_frequency: 611 # Update last log time 612 self._last_ui_update_time = emit_time 613 614 # Trigger Prompt Toolkit UI redraw. 615 self.redraw_ui() 616 617 def jump_to_error(self, backwards: bool = False) -> None: 618 if not self.root_log_pane.log_view.search_text: 619 self.root_log_pane.log_view.set_search_regex( 620 '^FAILED: ', False, None 621 ) 622 if backwards: 623 self.root_log_pane.log_view.search_backwards() 624 else: 625 self.root_log_pane.log_view.search_forwards() 626 self.root_log_pane.log_view.log_screen.reset_logs( 627 log_index=self.root_log_pane.log_view.log_index 628 ) 629 630 self.root_log_pane.log_view.move_selected_line_to_top() 631 632 def refresh_layout(self) -> None: 633 self.window_manager.update_root_container_body() 634 635 def update_menu_items(self): 636 """Required by the Window Manager Class.""" 637 638 def redraw_ui(self): 639 """Redraw the prompt_toolkit UI.""" 640 if hasattr(self, 'application'): 641 self.application.invalidate() 642 643 def focus_on_container(self, pane): 644 """Set application focus to a specific container.""" 645 # Try to focus on the given pane 646 try: 647 self.application.layout.focus(pane) 648 except ValueError: 649 # If the container can't be focused, focus on the first visible 650 # window pane. 651 self.window_manager.focus_first_visible_pane() 652 653 def focused_window(self): 654 """Return the currently focused window.""" 655 return self.application.layout.current_window 656 657 def focus_main_menu(self): 658 """Focus on the main menu. 659 660 Currently pw_watch has no main menu so focus on the first visible pane 661 instead.""" 662 self.window_manager.focus_first_visible_pane() 663 664 def switch_to_root_log(self) -> None: 665 ( 666 window_list, 667 pane_index, 668 ) = self.window_manager.find_window_list_and_pane_index( 669 self.root_log_pane 670 ) 671 window_list.switch_to_tab(pane_index) 672 673 def switch_to_build_log(self, log_index: int) -> None: 674 pane = self.recipe_index_to_log_pane.get(log_index, None) 675 if not pane: 676 return 677 678 ( 679 window_list, 680 pane_index, 681 ) = self.window_manager.find_window_list_and_pane_index(pane) 682 window_list.switch_to_tab(pane_index) 683 684 def command_runner_is_open(self) -> bool: 685 # pylint: disable=no-self-use 686 return False 687 688 def all_log_panes(self) -> Iterable[LogPane]: 689 for pane in self.window_manager.active_panes(): 690 if isinstance(pane, LogPane): 691 yield pane 692 693 def clear_log_panes(self) -> None: 694 """Erase all log pane content and turn on follow. 695 696 This is called whenever rebuilds occur. Either a manual build from 697 self.run_build or on file changes called from 698 pw_watch._handle_matched_event.""" 699 for pane in self.all_log_panes(): 700 pane.log_view.clear_visual_selection() 701 pane.log_view.clear_filters() 702 pane.log_view.log_store.clear_logs() 703 pane.log_view.view_mode_changed() 704 # Re-enable follow if needed 705 if not pane.log_view.follow: 706 pane.log_view.toggle_follow() 707 708 def run_build(self) -> None: 709 """Manually trigger a rebuild from the UI.""" 710 self.clear_log_panes() 711 self.event_handler.rebuild() 712 713 @property 714 def restart_on_changes(self) -> bool: 715 return self.event_handler.restart_on_changes 716 717 def toggle_restart_on_filechange(self) -> None: 718 self.event_handler.restart_on_changes = ( 719 not self.event_handler.restart_on_changes 720 ) 721 722 def get_status_bar_text(self) -> StyleAndTextTuples: 723 """Return formatted text for build status bar.""" 724 formatted_text: StyleAndTextTuples = [] 725 726 separator = ('', ' ') 727 name_width = self.event_handler.project_builder.max_name_width 728 729 # pylint: disable=protected-access 730 ( 731 _window_list, 732 pane, 733 ) = self.window_manager._get_active_window_list_and_pane() 734 # pylint: enable=protected-access 735 restarting = BUILDER_CONTEXT.restart_flag 736 737 for i, cfg in enumerate(self.event_handler.project_builder): 738 # The build directory 739 name_style = '' 740 if not pane: 741 formatted_text.append(('', '\n')) 742 continue 743 744 # Dim the build name if disabled 745 if not cfg.enabled: 746 name_style = 'class:theme-fg-inactive' 747 748 # If this build tab is selected, highlight with cyan. 749 if pane.pane_title() == cfg.display_name: 750 name_style = 'class:theme-fg-cyan' 751 752 formatted_text.append( 753 to_checkbox( 754 cfg.enabled, 755 functools.partial( 756 mouse_handlers.on_click, 757 cfg.toggle_enabled, 758 ), 759 end=' ', 760 unchecked_style='class:checkbox', 761 checked_style='class:checkbox-checked', 762 ) 763 ) 764 formatted_text.append( 765 ( 766 name_style, 767 f'{cfg.display_name}'.ljust(name_width), 768 functools.partial( 769 mouse_handlers.on_click, 770 functools.partial(self.switch_to_build_log, i), 771 ), 772 ) 773 ) 774 formatted_text.append(separator) 775 # Status 776 formatted_text.append(cfg.status.status_slug(restarting=restarting)) 777 formatted_text.append(separator) 778 # Current stdout line 779 formatted_text.extend(cfg.status.current_step_formatted()) 780 formatted_text.append(('', '\n')) 781 782 if not formatted_text: 783 formatted_text = [('', 'Loading...')] 784 785 self.set_tab_bar_colors() 786 787 return formatted_text 788 789 def set_tab_bar_colors(self) -> None: 790 restarting = BUILDER_CONTEXT.restart_flag 791 792 for cfg in BUILDER_CONTEXT.recipes: 793 pane = self.recipe_name_to_log_pane.get(cfg.display_name, None) 794 if not pane: 795 continue 796 797 pane.extra_tab_style = None 798 if not restarting and cfg.status.failed(): 799 pane.extra_tab_style = 'class:theme-fg-red' 800 801 def exit( 802 self, 803 exit_code: int = 1, 804 log_after_shutdown: Optional[Callable[[], None]] = None, 805 ) -> None: 806 _LOG.info('Exiting...') 807 BUILDER_CONTEXT.ctrl_c_pressed = True 808 809 # Shut everything down after the prompt_toolkit app exits. 810 def _really_exit(future: asyncio.Future) -> NoReturn: 811 BUILDER_CONTEXT.restore_logging_and_shutdown(log_after_shutdown) 812 os._exit(future.result()) # pylint: disable=protected-access 813 814 if self.application.future: 815 self.application.future.add_done_callback(_really_exit) 816 self.application.exit(result=exit_code) 817 818 def check_build_status(self) -> bool: 819 if not self.event_handler.current_stdout: 820 return False 821 822 if self._errors_in_output: 823 return True 824 825 if self.event_handler.current_build_errors > self._build_error_count: 826 self._errors_in_output = True 827 self.jump_to_error() 828 829 return True 830 831 def run(self): 832 self.plugin_start() 833 # Run the prompt_toolkit application 834 self.application.run(set_exception_handler=True) 835 836 def input_box_not_focused(self) -> Condition: 837 """Condition checking the focused control is not a text input field.""" 838 839 @Condition 840 def _test() -> bool: 841 """Check if the currently focused control is an input buffer. 842 843 Returns: 844 bool: True if the currently focused control is not a text input 845 box. For example if the user presses enter when typing in 846 the search box, return False. 847 """ 848 return not isinstance( 849 self.application.layout.current_control, BufferControl 850 ) 851 852 return _test 853 854 def modal_window_is_open(self): 855 """Return true if any modal window or dialog is open.""" 856 floating_window_is_open = ( 857 self.user_guide_window.show_window or self.quit_dialog.show_dialog 858 ) 859 860 floating_plugin_is_open = any( 861 plugin.show_pane for plugin in self.floating_window_plugins 862 ) 863 864 return floating_window_is_open or floating_plugin_is_open 865