aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnthony DiGirolamo <tonymd@google.com>2022-03-24 15:07:39 -0700
committerCQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>2022-04-01 22:39:24 +0000
commit92511aace1c2caaf521bc15c777fed19912aa4e9 (patch)
tree1d7e8925589ad599af7c69bcc89cbcedab35268b
parentdc88a4fafce6ca21b64166887c8692815aa947d1 (diff)
downloadpigweed-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.py18
-rw-r--r--pw_console/py/pw_console/widgets/border.py6
-rwxr-xr-xpw_watch/py/pw_watch/watch.py61
-rw-r--r--pw_watch/py/pw_watch/watch_app.py114
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: