diff options
author | Anthony DiGirolamo <tonymd@google.com> | 2022-03-24 15:07:39 -0700 |
---|---|---|
committer | CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2022-04-01 22:39:24 +0000 |
commit | 92511aace1c2caaf521bc15c777fed19912aa4e9 (patch) | |
tree | 1d7e8925589ad599af7c69bcc89cbcedab35268b | |
parent | dc88a4fafce6ca21b64166887c8692815aa947d1 (diff) | |
download | pigweed-92511aace1c2caaf521bc15c777fed19912aa4e9.tar.gz |
pw_watch: Fullscreen quit keybinding fix
- Use pw_console prefs to manage quit keybindings.
- Use quit confirm dialog box: Default binding is Ctrl-d.
- Show status of each build incrementally as they run.
Change-Id: Ibcb9695360eb3c7e4a93c6a2e3ca7570d1954edc
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/89040
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
-rw-r--r-- | pw_console/py/pw_console/quit_dialog.py | 18 | ||||
-rw-r--r-- | pw_console/py/pw_console/widgets/border.py | 6 | ||||
-rwxr-xr-x | pw_watch/py/pw_watch/watch.py | 61 | ||||
-rw-r--r-- | pw_watch/py/pw_watch/watch_app.py | 114 |
4 files changed, 145 insertions, 54 deletions
diff --git a/pw_console/py/pw_console/quit_dialog.py b/pw_console/py/pw_console/quit_dialog.py index e37ebc399..b466580eb 100644 --- a/pw_console/py/pw_console/quit_dialog.py +++ b/pw_console/py/pw_console/quit_dialog.py @@ -16,7 +16,8 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING +import sys +from typing import Optional, Callable, TYPE_CHECKING from prompt_toolkit.data_structures import Point from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent @@ -44,13 +45,18 @@ class QuitDialog(ConditionalContainer): DIALOG_HEIGHT = 2 - def __init__(self, application: ConsoleApp): + def __init__(self, + application: ConsoleApp, + on_quit: Optional[Callable] = None): self.application = application self.show_dialog = False # Tracks the last focused container, to enable restoring focus after # closing the dialog. self.last_focused_pane = None + self.on_quit_function = (on_quit if on_quit else + self._default_on_quit_function) + # Quit keybindings are active when this dialog is in focus key_bindings = KeyBindings() register = self.application.prefs.register_keybinding @@ -114,8 +120,14 @@ class QuitDialog(ConditionalContainer): self.focus_self() self.application.redraw_ui() + def _default_on_quit_function(self): + if hasattr(self.application, 'application'): + self.application.application.exit() + else: + sys.exit() + def quit_action(self): - self.application.application.exit() + self.on_quit_function() def get_action_fragments(self): """Return FormattedText with action buttons.""" diff --git a/pw_console/py/pw_console/widgets/border.py b/pw_console/py/pw_console/widgets/border.py index 64a03a3be..0cf1170ef 100644 --- a/pw_console/py/pw_console/widgets/border.py +++ b/pw_console/py/pw_console/widgets/border.py @@ -13,7 +13,7 @@ # the License. """Wrapper fuctions to add borders around prompt_toolkit containers.""" -from typing import List, Optional +from typing import Callable, List, Optional, Union from prompt_toolkit.layout import ( AnyContainer, @@ -29,8 +29,8 @@ def create_border( content: AnyContainer, content_height: Optional[int] = None, title: str = '', - border_style: str = '', - base_style: str = '', + border_style: Union[Callable[[], str], str] = '', + base_style: Union[Callable[[], str], str] = '', top: bool = True, bottom: bool = True, left: bool = True, diff --git a/pw_watch/py/pw_watch/watch.py b/pw_watch/py/pw_watch/watch.py index 8f3d29152..fe495efba 100755 --- a/pw_watch/py/pw_watch/watch.py +++ b/pw_watch/py/pw_watch/watch.py @@ -38,6 +38,7 @@ Usage examples: import argparse from dataclasses import dataclass import errno +from itertools import zip_longest import logging import os from pathlib import Path @@ -109,6 +110,8 @@ _FAIL_MESSAGE = """ ░ ░ ░ ░ ░ """ +_FULLSCREEN_STATUS_COLUMN_WIDTH = 10 + # TODO(keir): Figure out a better strategy for exiting. The problem with the # watcher is that doing a "clean exit" is slow. However, by directly exiting, @@ -292,8 +295,10 @@ class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction): self._clear_screen() if self.fullscreen_enabled: - msg = 'Watching for changes. Ctrl-C to exit; enter to rebuild' - self.result_message = [('', msg)] + self.create_result_message() + _LOG.info( + _COLOR.green( + 'Watching for changes. Ctrl-d to exit; enter to rebuild')) else: for line in pw_cli.branding.banner().splitlines(): _LOG.info(line) @@ -327,6 +332,33 @@ class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction): tag = '(FAIL)' _LOG.log(level, '%s Finished build: %s %s', index, cmd, tag) + self.create_result_message() + + def create_result_message(self): + if not self.fullscreen_enabled: + return + + self.result_message = [] + first_building_target_found = False + for (succeeded, command) in zip_longest(self.builds_succeeded, + self.build_commands): + if succeeded: + self.result_message.append( + ('class:theme-fg-green', + 'OK'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH))) + elif succeeded is None and not first_building_target_found: + first_building_target_found = True + self.result_message.append( + ('class:theme-fg-yellow', + 'Building'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH))) + elif first_building_target_found: + self.result_message.append( + ('', ''.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH))) + else: + self.result_message.append( + ('class:theme-fg-red', + 'Failed'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH))) + self.result_message.append(('', f' {command}\n')) def _run_build(self, index: str, cmd: BuildCommand, env: dict) -> bool: # Make sure there is a build.ninja file for Ninja to use. @@ -352,7 +384,9 @@ class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction): def _execute_command(self, command: list, env: dict) -> bool: """Runs a command with a blank before/after for visual separation.""" self.current_build_errors = 0 - self.status_message = ('ansiyellow', 'Building') + self.status_message = ( + 'class:theme-fg-yellow', + 'Building'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH)) if self.fullscreen_enabled: return self._execute_command_watch_app(command, env) print() @@ -432,26 +466,23 @@ class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction): def on_complete(self, cancelled: bool = False) -> None: # First, use the standard logging facilities to report build status. if cancelled: - self.status_message = ('', 'Cancelled') + self.status_message = ( + '', 'Cancelled'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH)) _LOG.error('Finished; build was interrupted') elif all(self.builds_succeeded): - self.status_message = ('ansigreen', 'Succeeded') + self.status_message = ( + 'class:theme-fg-green', + 'Succeeded'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH)) _LOG.info('Finished; all successful') else: - self.status_message = ('ansired', 'Failed') + self.status_message = ( + 'class:theme-fg-red', + 'Failed'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH)) _LOG.info('Finished; some builds failed') # Show individual build results for fullscreen app if self.fullscreen_enabled: - self.result_message = [] - for (succeeded, cmd) in zip(self.builds_succeeded, - self.build_commands): - if succeeded: - self.result_message.append( - ('class:theme-fg-green', 'OK ')) - else: - self.result_message.append(('class:theme-fg-red', 'FAIL')) - self.result_message.append(('', f' {cmd}\n')) + self.create_result_message() # For non-fullscreen pw watch else: # Show a more distinct colored banner. diff --git a/pw_watch/py/pw_watch/watch_app.py b/pw_watch/py/pw_watch/watch_app.py index 0f8c93f0f..61f7fd99c 100644 --- a/pw_watch/py/pw_watch/watch_app.py +++ b/pw_watch/py/pw_watch/watch_app.py @@ -37,6 +37,8 @@ from prompt_toolkit.key_binding import ( from prompt_toolkit.layout import ( Dimension, DynamicContainer, + Float, + FloatContainer, FormattedTextControl, HSplit, Layout, @@ -51,7 +53,9 @@ from pw_console.console_prefs import ConsolePrefs from pw_console.get_pw_console_app import PW_CONSOLE_APP_CONTEXTVAR from pw_console.log_pane import LogPane from pw_console.plugin_mixin import PluginMixin +from pw_console.quit_dialog import QuitDialog import pw_console.style +import pw_console.widgets.border from pw_console.window_manager import WindowManager _NINJA_LOG = logging.getLogger('pw_watch_ninja_output') @@ -90,18 +94,7 @@ class WatchApp(PluginMixin): self.prefs = ConsolePrefs() - key_bindings = KeyBindings() - - @key_bindings.add('c-c', filter=self.input_box_not_focused()) - def _quit(_event): - "Quit." - _LOG.info('Got quit signal; exiting...') - self.exit(0) - - @key_bindings.add('enter', filter=self.input_box_not_focused()) - def _run_build(_event): - "Rebuild." - self.run_build() + self.quit_dialog = QuitDialog(self, self.exit) # type: ignore self.search_history_filename = self.prefs.search_history # History instance for search toolbars. @@ -156,22 +149,64 @@ class WatchApp(PluginMixin): self.window_manager_container = ( self.window_manager.create_root_container()) - self.root_container = HSplit([ - # The top toolbar. - Window( - content=FormattedTextControl(self.get_statusbar_text), - height=Dimension.exact(1), - style='class:toolbar_inactive', - ), - # Result Toolbar. - Window( - content=FormattedTextControl(self.get_resultbar_text), - height=lambda: len(self.event_handler.build_commands), - style='class:toolbar_inactive', - ), - # The main content. - DynamicContainer(lambda: self.window_manager_container), - ]) + self.status_bar_border_style = 'class:command-runner-border' + + self.root_container = FloatContainer( + HSplit([ + pw_console.widgets.border.create_border( + HSplit([ + # The top toolbar. + Window( + content=FormattedTextControl( + self.get_statusbar_text), + height=Dimension.exact(1), + style='class:toolbar_inactive', + ), + # Result Toolbar. + Window( + content=FormattedTextControl( + self.get_resultbar_text), + height=lambda: len(self.event_handler. + build_commands), + style='class:toolbar_inactive', + ), + ]), + border_style=lambda: self.status_bar_border_style, + base_style='class:toolbar_inactive', + left_margin_columns=1, + right_margin_columns=1, + ), + # The main content. + DynamicContainer(lambda: self.window_manager_container), + ]), + floats=[ + Float( + content=self.quit_dialog, + top=2, + left=2, + ), + ], + ) + + key_bindings = KeyBindings() + + @key_bindings.add('enter', filter=self.input_box_not_focused()) + def _run_build(_event): + "Rebuild." + self.run_build() + + register = self.prefs.register_keybinding + + @register('global.exit-no-confirmation', key_bindings) + def _quit_no_confirm(_event): + """Quit without confirmation.""" + _LOG.info('Got quit signal; exiting...') + self.exit(0) + + @register('global.exit-with-confirmation', key_bindings) + def _quit_with_confirm(_event): + """Quit with confirmation dialog.""" + self.quit_dialog.open_dialog() self.key_bindings = merge_key_bindings([ self.window_manager.key_bindings, @@ -185,9 +220,11 @@ class WatchApp(PluginMixin): Style.from_dict({'search': 'bg:ansired ansiblack'}), ]) + self.layout = Layout(self.root_container, + focused_element=self.ninja_log_pane) + self.application: Application = Application( - layout=Layout(self.root_container, - focused_element=self.ninja_log_pane), + layout=self.layout, key_bindings=self.key_bindings, mouse_support=True, color_depth=self.color_depth, @@ -200,7 +237,7 @@ class WatchApp(PluginMixin): self.plugin_init( plugin_callback=self.check_build_status, - plugin_callback_frequency=1.0, + plugin_callback_frequency=0.5, plugin_logger_name='pw_watch_stdout_checker', ) @@ -229,6 +266,14 @@ class WatchApp(PluginMixin): """Set application focus to a specific container.""" self.application.layout.focus(pane) + def focused_window(self): + """Return the currently focused window.""" + return self.application.layout.current_window + + def command_runner_is_open(self) -> bool: + # pylint: disable=no-self-use + return False + def clear_ninja_log(self) -> None: self.ninja_log_view.log_store.clear_logs() self.ninja_log_view._restart_filtering() # pylint: disable=protected-access @@ -249,20 +294,23 @@ class WatchApp(PluginMixin): is_building = False if status: fragments = [status] - is_building = status[1] == 'Building' + is_building = status[1].endswith('Building') separator = ('', ' ') + self.status_bar_border_style = 'class:theme-fg-green' if is_building: percent = self.event_handler.current_build_percent percent *= 100 fragments.append(separator) fragments.append(('ansicyan', '{:.0f}%'.format(percent))) + self.status_bar_border_style = 'class:theme-fg-yellow' if self.event_handler.current_build_errors > 0: fragments.append(separator) fragments.append(('', 'Errors:')) fragments.append( ('ansired', str(self.event_handler.current_build_errors))) + self.status_bar_border_style = 'class:theme-fg-red' if is_building: fragments.append(separator) @@ -276,7 +324,7 @@ class WatchApp(PluginMixin): result = [('', 'Loading...')] return result - def exit(self, exit_code: int) -> None: + def exit(self, exit_code: int = 0) -> None: log_file = self.external_logfile def _really_exit(future: asyncio.Future) -> NoReturn: |