• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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