blob: 625e381d8bded64b9e01f9f19df059f630d05ec5 [file] [log] [blame]
# Copyright 2021 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""ConsoleApp control class."""
import asyncio
import builtins
import functools
import importlib.resources
import logging
import os
from pathlib import Path
import sys
import time
from threading import Thread
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
from jinja2 import Environment, DictLoader, make_logging_undefined
from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.output import ColorDepth
from prompt_toolkit.application import Application
from prompt_toolkit.filters import Condition
from prompt_toolkit.styles import (
DynamicStyle,
merge_styles,
)
from prompt_toolkit.layout import (
ConditionalContainer,
Float,
Layout,
)
from prompt_toolkit.widgets import FormattedTextToolbar
from prompt_toolkit.widgets import (
MenuContainer,
MenuItem,
)
from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings
from prompt_toolkit.history import (
FileHistory,
History,
ThreadedHistory,
)
from ptpython.layout import CompletionVisualisation # type: ignore
from ptpython.key_bindings import ( # type: ignore
load_python_bindings, load_sidebar_bindings,
)
from pw_console.command_runner import CommandRunner
from pw_console.console_prefs import ConsolePrefs
from pw_console.help_window import HelpWindow
import pw_console.key_bindings
from pw_console.log_pane import LogPane
from pw_console.log_store import LogStore
from pw_console.pw_ptpython_repl import PwPtPythonRepl
from pw_console.python_logging import all_loggers
from pw_console.quit_dialog import QuitDialog
from pw_console.repl_pane import ReplPane
import pw_console.style
from pw_console.test_mode import start_fake_logger
import pw_console.widgets.checkbox
from pw_console.widgets import FloatingWindowPane
import pw_console.widgets.mouse_handlers
from pw_console.window_manager import WindowManager
_LOG = logging.getLogger(__package__)
_ROOT_LOG = logging.getLogger('')
_SYSTEM_COMMAND_LOG = logging.getLogger('pw_console_system_command')
MAX_FPS = 30
MIN_REDRAW_INTERVAL = (60.0 / MAX_FPS) / 60.0
class FloatingMessageBar(ConditionalContainer):
"""Floating message bar for showing status messages."""
def __init__(self, application):
super().__init__(
FormattedTextToolbar(
(lambda: application.message if application.message else []),
style='class:toolbar_inactive',
),
filter=Condition(
lambda: application.message and application.message != ''))
def _add_log_handler_to_pane(logger: Union[str, logging.Logger],
pane: 'LogPane',
level_name: Optional[str] = None) -> None:
"""A log pane handler for a given logger instance."""
if not pane:
return
pane.add_log_handler(logger, level_name=level_name)
def get_default_colordepth(
color_depth: Optional[ColorDepth] = None) -> ColorDepth:
# Set prompt_toolkit color_depth to the highest possible.
if color_depth is None:
# Default to 24bit color
color_depth = ColorDepth.DEPTH_24_BIT
# If using Apple Terminal switch to 256 (8bit) color.
term_program = os.environ.get('TERM_PROGRAM', '')
if sys.platform == 'darwin' and 'Apple_Terminal' in term_program:
color_depth = ColorDepth.DEPTH_8_BIT
# Check for any PROMPT_TOOLKIT_COLOR_DEPTH environment variables
color_depth_override = os.environ.get('PROMPT_TOOLKIT_COLOR_DEPTH', '')
if color_depth_override:
color_depth = ColorDepth(color_depth_override)
return color_depth
class ConsoleApp:
"""The main ConsoleApp class that glues everything together."""
# pylint: disable=too-many-instance-attributes,too-many-public-methods
def __init__(
self,
global_vars=None,
local_vars=None,
repl_startup_message=None,
help_text=None,
app_title=None,
color_depth=None,
extra_completers=None,
prefs=None,
floating_window_plugins: Optional[List[Tuple[FloatingWindowPane,
Dict]]] = None,
):
self.prefs = prefs if prefs else ConsolePrefs()
self.color_depth = get_default_colordepth(color_depth)
# Max frequency in seconds of prompt_toolkit UI redraws triggered by new
# log lines.
self.log_ui_update_frequency = 0.1 # 10 FPS
self._last_ui_update_time = time.time()
# Create a default global and local symbol table. Values are the same
# structure as what is returned by globals():
# https://docs.python.org/3/library/functions.html#globals
if global_vars is None:
global_vars = {
'__name__': '__main__',
'__package__': None,
'__doc__': None,
'__builtins__': builtins,
}
local_vars = local_vars or global_vars
jinja_templates = {
t: importlib.resources.read_text('pw_console.templates', t)
for t in importlib.resources.contents('pw_console.templates')
if t.endswith('.jinja')
}
# Setup the Jinja environment
self.jinja_env = Environment(
# Load templates automatically from pw_console/templates
loader=DictLoader(jinja_templates),
# Raise errors if variables are undefined in templates
undefined=make_logging_undefined(
logger=logging.getLogger(__package__), ),
# Trim whitespace in templates
trim_blocks=True,
lstrip_blocks=True,
)
self.repl_history_filename = self.prefs.repl_history
self.search_history_filename = self.prefs.search_history
# History instance for search toolbars.
self.search_history: History = ThreadedHistory(
FileHistory(self.search_history_filename))
# Event loop for executing user repl code.
self.user_code_loop = asyncio.new_event_loop()
self.test_mode_log_loop = asyncio.new_event_loop()
self.app_title = app_title if app_title else 'Pigweed Console'
# Top level UI state toggles.
self.load_theme(self.prefs.ui_theme)
# Pigweed upstream RST user guide
self.user_guide_window = HelpWindow(self, title='User Guide')
self.user_guide_window.load_user_guide()
# Top title message
self.message = [('class:logo', self.app_title), ('', ' ')]
self.message.extend(
pw_console.widgets.checkbox.to_keybind_indicator(
'Ctrl-p',
'Search Menu',
functools.partial(pw_console.widgets.mouse_handlers.on_click,
self.open_command_runner_main_menu),
base_style='class:toolbar-button-inactive',
))
# One space separator
self.message.append(('', ' '))
# Auto-generated keybindings list for all active panes
self.keybind_help_window = HelpWindow(self, title='Keyboard Shortcuts')
# Downstream project specific help text
self.app_help_text = help_text if help_text else None
self.app_help_window = HelpWindow(self,
additional_help_text=help_text,
title=(self.app_title + ' Help'))
self.app_help_window.generate_help_text()
self.prefs_file_window = HelpWindow(self, title='.pw_console.yaml')
self.prefs_file_window.load_yaml_text(
self.prefs.current_config_as_yaml())
self.floating_window_plugins: List[FloatingWindowPane] = []
if floating_window_plugins:
self.floating_window_plugins = [
plugin for plugin, _ in floating_window_plugins
]
# Used for tracking which pane was in focus before showing help window.
self.last_focused_pane = None
# Create a ptpython repl instance.
self.pw_ptpython_repl = PwPtPythonRepl(
get_globals=lambda: global_vars,
get_locals=lambda: local_vars,
color_depth=self.color_depth,
history_filename=self.repl_history_filename,
extra_completers=extra_completers,
)
self.input_history = self.pw_ptpython_repl.history
self.repl_pane = ReplPane(
application=self,
python_repl=self.pw_ptpython_repl,
startup_message=repl_startup_message,
)
self.pw_ptpython_repl.use_code_colorscheme(self.prefs.code_theme)
self.system_command_output_pane: Optional[LogPane] = None
if self.prefs.swap_light_and_dark:
self.toggle_light_theme()
# Window panes are added via the window_manager
self.window_manager = WindowManager(self)
self.window_manager.add_pane_no_checks(self.repl_pane)
# Top of screen menu items
self.menu_items = self._create_menu_items()
self.quit_dialog = QuitDialog(self)
# Key bindings registry.
self.key_bindings = pw_console.key_bindings.create_key_bindings(self)
# Create help window text based global key_bindings and active panes.
self._update_help_window()
self.command_runner = CommandRunner(
self,
width=self.prefs.command_runner_width,
height=self.prefs.command_runner_height,
)
self.floats = [
# Top message bar
Float(
content=FloatingMessageBar(self),
top=0,
right=0,
height=1,
),
# Centered floating help windows
Float(
content=self.prefs_file_window,
top=2,
bottom=2,
# Callable to get width
width=self.prefs_file_window.content_width,
),
Float(
content=self.app_help_window,
top=2,
bottom=2,
# Callable to get width
width=self.app_help_window.content_width,
),
Float(
content=self.user_guide_window,
top=2,
bottom=2,
# Callable to get width
width=self.user_guide_window.content_width,
),
Float(
content=self.keybind_help_window,
top=2,
bottom=2,
# Callable to get width
width=self.keybind_help_window.content_width,
),
]
if floating_window_plugins:
self.floats.extend([
Float(content=plugin_container, **float_args)
for plugin_container, float_args in floating_window_plugins
])
self.floats.extend([
# Completion menu that can overlap other panes since it lives in
# the top level Float container.
Float(
xcursor=True,
ycursor=True,
content=ConditionalContainer(
content=CompletionsMenu(
scroll_offset=(lambda: self.pw_ptpython_repl.
completion_menu_scroll_offset),
max_height=16,
),
# Only show our completion if ptpython's is disabled.
filter=Condition(
lambda: self.pw_ptpython_repl.completion_visualisation
== CompletionVisualisation.NONE),
),
),
Float(
content=self.command_runner,
# Callable to get width
width=self.command_runner.content_width,
**self.prefs.command_runner_position,
),
Float(
content=self.quit_dialog,
top=2,
left=2,
),
])
# prompt_toolkit root container.
self.root_container = MenuContainer(
body=self.window_manager.create_root_container(),
menu_items=self.menu_items,
floats=self.floats,
)
# NOTE: ptpython stores it's completion menus in this HSplit:
#
# self.pw_ptpython_repl.__pt_container__()
# .children[0].children[0].children[0].floats[0].content.children
#
# Index 1 is a CompletionsMenu and is shown when:
# self.pw_ptpython_repl
# .completion_visualisation == CompletionVisualisation.POP_UP
#
# Index 2 is a MultiColumnCompletionsMenu and is shown when:
# self.pw_ptpython_repl
# .completion_visualisation == CompletionVisualisation.MULTI_COLUMN
#
# Setup the prompt_toolkit layout with the repl pane as the initially
# focused element.
self.layout: Layout = Layout(
self.root_container,
focused_element=self.pw_ptpython_repl,
)
# Create the prompt_toolkit Application instance.
self.application: Application = Application(
layout=self.layout,
key_bindings=merge_key_bindings([
# Pull key bindings from ptpython
load_python_bindings(self.pw_ptpython_repl),
load_sidebar_bindings(self.pw_ptpython_repl),
self.window_manager.key_bindings,
self.key_bindings,
]),
style=DynamicStyle(lambda: merge_styles([
self._current_theme,
# Include ptpython styles
self.pw_ptpython_repl._current_style, # pylint: disable=protected-access
])),
style_transformation=self.pw_ptpython_repl.style_transformation,
enable_page_navigation_bindings=True,
full_screen=True,
mouse_support=True,
color_depth=self.color_depth,
clipboard=PyperclipClipboard(),
min_redraw_interval=MIN_REDRAW_INTERVAL,
)
def get_template(self, file_name: str):
return self.jinja_env.get_template(file_name)
def run_pane_menu_option(self, function_to_run):
# Run the function for a particular menu item.
return_value = function_to_run()
# It's return value dictates if the main menu should close or not.
# - False: The main menu stays open. This is the default prompt_toolkit
# menu behavior.
# - True: The main menu closes.
# Update menu content. This will refresh checkboxes and add/remove
# items.
self.update_menu_items()
# Check if the main menu should stay open.
if not return_value:
# Keep the main menu open
self.focus_main_menu()
def open_new_log_pane_for_logger(
self,
logger_name: str,
level_name='NOTSET',
window_title: Optional[str] = None) -> None:
pane_title = window_title if window_title else logger_name
self.run_pane_menu_option(
functools.partial(self.add_log_handler,
pane_title, [logger_name],
log_level_name=level_name))
def set_ui_theme(self, theme_name: str) -> Callable:
call_function = functools.partial(
self.run_pane_menu_option,
functools.partial(self.load_theme, theme_name))
return call_function
def set_code_theme(self, theme_name: str) -> Callable:
call_function = functools.partial(
self.run_pane_menu_option,
functools.partial(self.pw_ptpython_repl.use_code_colorscheme,
theme_name))
return call_function
def update_menu_items(self):
self.menu_items = self._create_menu_items()
self.root_container.menu_items = self.menu_items
def open_command_runner_main_menu(self) -> None:
self.command_runner.set_completions()
if not self.command_runner_is_open():
self.command_runner.open_dialog()
def open_command_runner_loggers(self) -> None:
self.command_runner.set_completions(
window_title='Open Logger',
load_completions=self._create_logger_completions)
if not self.command_runner_is_open():
self.command_runner.open_dialog()
def _create_logger_completions(self) -> List[Tuple[str, Callable]]:
completions: List[Tuple[str, Callable]] = [
(
'root',
functools.partial(self.open_new_log_pane_for_logger,
'',
window_title='root'),
),
]
all_logger_names = sorted([logger.name for logger in all_loggers()])
for logger_name in all_logger_names:
completions.append((
logger_name,
functools.partial(self.open_new_log_pane_for_logger,
logger_name),
))
return completions
def open_command_runner_history(self) -> None:
self.command_runner.set_completions(
window_title='History',
load_completions=self._create_history_completions)
if not self.command_runner_is_open():
self.command_runner.open_dialog()
def _create_history_completions(self) -> List[Tuple[str, Callable]]:
return [(
description,
functools.partial(self.repl_pane.insert_text_into_input_buffer,
text),
) for description, text in self.repl_pane.history_completions()]
def open_command_runner_snippets(self) -> None:
self.command_runner.set_completions(
window_title='Snippets',
load_completions=self._create_snippet_completions)
if not self.command_runner_is_open():
self.command_runner.open_dialog()
def _create_snippet_completions(self) -> List[Tuple[str, Callable]]:
completions: List[Tuple[str, Callable]] = [(
description,
functools.partial(self.repl_pane.insert_text_into_input_buffer,
text),
) for description, text in self.prefs.snippet_completions()]
return completions
def _create_menu_items(self):
themes_submenu = [
MenuItem('Toggle Light/Dark', handler=self.toggle_light_theme),
MenuItem('-'),
MenuItem(
'UI Themes',
children=[
MenuItem('Default: Dark', self.set_ui_theme('dark')),
MenuItem('High Contrast',
self.set_ui_theme('high-contrast-dark')),
MenuItem('Nord', self.set_ui_theme('nord')),
MenuItem('Nord Light', self.set_ui_theme('nord-light')),
MenuItem('Moonlight', self.set_ui_theme('moonlight')),
],
),
MenuItem(
'Code Themes',
children=[
MenuItem('Code: pigweed-code',
self.set_code_theme('pigweed-code')),
MenuItem('Code: pigweed-code-light',
self.set_code_theme('pigweed-code-light')),
MenuItem('Code: material',
self.set_code_theme('material')),
MenuItem('Code: gruvbox-light',
self.set_code_theme('gruvbox-light')),
MenuItem('Code: gruvbox-dark',
self.set_code_theme('gruvbox-dark')),
MenuItem('Code: tomorrow-night',
self.set_code_theme('tomorrow-night')),
MenuItem('Code: tomorrow-night-bright',
self.set_code_theme('tomorrow-night-bright')),
MenuItem('Code: tomorrow-night-blue',
self.set_code_theme('tomorrow-night-blue')),
MenuItem('Code: tomorrow-night-eighties',
self.set_code_theme('tomorrow-night-eighties')),
MenuItem('Code: dracula', self.set_code_theme('dracula')),
MenuItem('Code: zenburn', self.set_code_theme('zenburn')),
],
),
]
file_menu = [
# File menu
MenuItem(
'[File]',
children=[
MenuItem('Insert Repl Snippet',
handler=self.open_command_runner_snippets),
MenuItem('Insert Repl History',
handler=self.open_command_runner_history),
MenuItem('Open Logger',
handler=self.open_command_runner_loggers),
MenuItem(
'Log Table View',
children=[
MenuItem(
'{check} Hide Date'.format(
check=pw_console.widgets.checkbox.
to_checkbox_text(
self.prefs.hide_date_from_log_time,
end='')),
handler=functools.partial(
self.run_pane_menu_option,
functools.partial(
self.toggle_pref_option,
'hide_date_from_log_time')),
),
MenuItem(
'{check} Show Source File'.format(
check=pw_console.widgets.checkbox.
to_checkbox_text(
self.prefs.show_source_file, end='')),
handler=functools.partial(
self.run_pane_menu_option,
functools.partial(self.toggle_pref_option,
'show_source_file')),
),
MenuItem(
'{check} Show Python File'.format(
check=pw_console.widgets.checkbox.
to_checkbox_text(
self.prefs.show_python_file, end='')),
handler=functools.partial(
self.run_pane_menu_option,
functools.partial(self.toggle_pref_option,
'show_python_file')),
),
MenuItem(
'{check} Show Python Logger'.format(
check=pw_console.widgets.checkbox.
to_checkbox_text(
self.prefs.show_python_logger,
end='')),
handler=functools.partial(
self.run_pane_menu_option,
functools.partial(self.toggle_pref_option,
'show_python_logger')),
),
]),
MenuItem('-'),
MenuItem(
'Themes',
children=themes_submenu,
),
MenuItem('-'),
MenuItem('Exit', handler=self.exit_console),
],
),
]
edit_menu = [
MenuItem(
'[Edit]',
children=[
MenuItem('Paste to Python Input',
handler=self.repl_pane.
paste_system_clipboard_to_input_buffer),
MenuItem('-'),
MenuItem('Copy all Python Output',
handler=self.repl_pane.copy_all_output_text),
MenuItem('Copy all Python Input',
handler=self.repl_pane.copy_all_input_text),
MenuItem('-'),
MenuItem('Clear Python Input',
self.repl_pane.clear_input_buffer),
MenuItem('Clear Python Output',
self.repl_pane.clear_output_buffer),
],
),
]
view_menu = [
MenuItem(
'[View]',
children=[
# [Menu Item ][Keybind ]
MenuItem('Focus Next Window/Tab Ctrl-Alt-n',
handler=self.window_manager.focus_next_pane),
# [Menu Item ][Keybind ]
MenuItem('Focus Prev Window/Tab Ctrl-Alt-p',
handler=self.window_manager.focus_previous_pane),
MenuItem('-'),
# [Menu Item ][Keybind ]
MenuItem('Move Window Up Ctrl-Alt-Up',
handler=functools.partial(
self.run_pane_menu_option,
self.window_manager.move_pane_up)),
# [Menu Item ][Keybind ]
MenuItem('Move Window Down Ctrl-Alt-Down',
handler=functools.partial(
self.run_pane_menu_option,
self.window_manager.move_pane_down)),
# [Menu Item ][Keybind ]
MenuItem('Move Window Left Ctrl-Alt-Left',
handler=functools.partial(
self.run_pane_menu_option,
self.window_manager.move_pane_left)),
# [Menu Item ][Keybind ]
MenuItem('Move Window Right Ctrl-Alt-Right',
handler=functools.partial(
self.run_pane_menu_option,
self.window_manager.move_pane_right)),
MenuItem('-'),
# [Menu Item ][Keybind ]
MenuItem('Shrink Height Alt-Minus',
handler=functools.partial(
self.run_pane_menu_option,
self.window_manager.shrink_pane)),
# [Menu Item ][Keybind ]
MenuItem('Enlarge Height Alt-=',
handler=functools.partial(
self.run_pane_menu_option,
self.window_manager.enlarge_pane)),
MenuItem('-'),
# [Menu Item ][Keybind ]
MenuItem('Shrink Column Alt-,',
handler=functools.partial(
self.run_pane_menu_option,
self.window_manager.shrink_split)),
# [Menu Item ][Keybind ]
MenuItem('Enlarge Column Alt-.',
handler=functools.partial(
self.run_pane_menu_option,
self.window_manager.enlarge_split)),
MenuItem('-'),
# [Menu Item ][Keybind ]
MenuItem('Balance Window Sizes Ctrl-u',
handler=functools.partial(
self.run_pane_menu_option,
self.window_manager.balance_window_sizes)),
],
),
]
window_menu_items = self.window_manager.create_window_menu_items()
floating_window_items = []
if self.floating_window_plugins:
floating_window_items.append(MenuItem('-', None))
floating_window_items.extend(
MenuItem(
'Floating Window {index}: {title}'.format(
index=pane_index + 1,
title=pane.menu_title(),
),
children=[
MenuItem(
'{check} Show/Hide Window'.format(
check=pw_console.widgets.checkbox.
to_checkbox_text(pane.show_pane, end='')),
handler=functools.partial(
self.run_pane_menu_option, pane.toggle_dialog),
),
] + [
MenuItem(text,
handler=functools.partial(
self.run_pane_menu_option, handler))
for text, handler in pane.get_window_menu_options()
],
) for pane_index, pane in enumerate(
self.floating_window_plugins))
window_menu_items.extend(floating_window_items)
window_menu = [MenuItem('[Windows]', children=window_menu_items)]
top_level_plugin_menus = []
for pane in self.window_manager.active_panes():
top_level_plugin_menus.extend(pane.get_top_level_menus())
if self.floating_window_plugins:
for pane in self.floating_window_plugins:
top_level_plugin_menus.extend(pane.get_top_level_menus())
help_menu_items = [
MenuItem(self.user_guide_window.menu_title(),
handler=self.user_guide_window.toggle_display),
MenuItem(self.keybind_help_window.menu_title(),
handler=self.keybind_help_window.toggle_display),
MenuItem('-'),
MenuItem('View Key Binding Config',
handler=self.prefs_file_window.toggle_display),
]
if self.app_help_text:
help_menu_items.extend([
MenuItem('-'),
MenuItem(self.app_help_window.menu_title(),
handler=self.app_help_window.toggle_display)
])
help_menu = [
# Info / Help
MenuItem(
'[Help]',
children=help_menu_items,
),
]
return (file_menu + edit_menu + view_menu + top_level_plugin_menus +
window_menu + help_menu)
def focus_main_menu(self):
"""Set application focus to the main menu."""
self.application.layout.focus(self.root_container.window)
def focus_on_container(self, pane):
"""Set application focus to a specific container."""
# Try to focus on the given pane
try:
self.application.layout.focus(pane)
except ValueError:
# If the container can't be focused, focus on the first visible
# window pane.
self.window_manager.focus_first_visible_pane()
def toggle_light_theme(self):
"""Toggle light and dark theme colors."""
# Use ptpython's style_transformation to swap dark and light colors.
self.pw_ptpython_repl.swap_light_and_dark = (
not self.pw_ptpython_repl.swap_light_and_dark)
if self.application:
self.focus_main_menu()
def toggle_pref_option(self, setting_name):
self.prefs.toggle_bool_option(setting_name)
def load_theme(self, theme_name=None):
"""Regenerate styles for the current theme_name."""
self._current_theme = pw_console.style.generate_styles(theme_name)
if theme_name:
self.prefs.set_ui_theme(theme_name)
def _create_log_pane(self,
title: str = '',
log_store: Optional[LogStore] = None) -> 'LogPane':
# Create one log pane.
log_pane = LogPane(application=self,
pane_title=title,
log_store=log_store)
self.window_manager.add_pane(log_pane)
return log_pane
def load_clean_config(self, config_file: Path) -> None:
self.prefs.reset_config()
self.prefs.load_config_file(config_file)
def apply_window_config(self) -> None:
self.window_manager.apply_config(self.prefs)
def refresh_layout(self) -> None:
self.window_manager.update_root_container_body()
self.update_menu_items()
self._update_help_window()
def add_log_handler(
self,
window_title: str,
logger_instances: Union[Iterable[logging.Logger], LogStore],
separate_log_panes: bool = False,
log_level_name: Optional[str] = None) -> Optional[LogPane]:
"""Add the Log pane as a handler for this logger instance."""
existing_log_pane = None
# Find an existing LogPane with the same window_title.
for pane in self.window_manager.active_panes():
if isinstance(pane, LogPane) and pane.pane_title() == window_title:
existing_log_pane = pane
break
log_store: Optional[LogStore] = None
if isinstance(logger_instances, LogStore):
log_store = logger_instances
if not existing_log_pane or separate_log_panes:
existing_log_pane = self._create_log_pane(title=window_title,
log_store=log_store)
if isinstance(logger_instances, list):
for logger in logger_instances:
_add_log_handler_to_pane(logger, existing_log_pane,
log_level_name)
self.refresh_layout()
return existing_log_pane
def _user_code_thread_entry(self):
"""Entry point for the user code thread."""
asyncio.set_event_loop(self.user_code_loop)
self.user_code_loop.run_forever()
def start_user_code_thread(self):
"""Create a thread for running user code so the UI isn't blocked."""
thread = Thread(target=self._user_code_thread_entry,
args=(),
daemon=True)
thread.start()
def _test_mode_log_thread_entry(self):
"""Entry point for the user code thread."""
asyncio.set_event_loop(self.test_mode_log_loop)
self.test_mode_log_loop.run_forever()
def _update_help_window(self):
"""Generate the help window text based on active pane keybindings."""
# Add global mouse bindings to the help text.
mouse_functions = {
'Focus pane, menu or log line.': ['Click'],
'Scroll current window.': ['Scroll wheel'],
}
self.keybind_help_window.add_custom_keybinds_help_text(
'Global Mouse', mouse_functions)
# Add global key bindings to the help text.
self.keybind_help_window.add_keybind_help_text('Global',
self.key_bindings)
self.keybind_help_window.add_keybind_help_text(
'Window Management', self.window_manager.key_bindings)
# Add activated plugin key bindings to the help text.
for pane in self.window_manager.active_panes():
for key_bindings in pane.get_all_key_bindings():
help_section_title = pane.__class__.__name__
if isinstance(key_bindings, KeyBindings):
self.keybind_help_window.add_keybind_help_text(
help_section_title, key_bindings)
elif isinstance(key_bindings, dict):
self.keybind_help_window.add_custom_keybinds_help_text(
help_section_title, key_bindings)
self.keybind_help_window.generate_help_text()
def toggle_log_line_wrapping(self):
"""Menu item handler to toggle line wrapping of all log panes."""
for pane in self.window_manager.active_panes():
if isinstance(pane, LogPane):
pane.toggle_wrap_lines()
def focused_window(self):
"""Return the currently focused window."""
return self.application.layout.current_window
def command_runner_is_open(self) -> bool:
return self.command_runner.show_dialog
def command_runner_last_focused_pane(self) -> Any:
return self.command_runner.last_focused_pane
def modal_window_is_open(self):
"""Return true if any modal window or dialog is open."""
floating_window_is_open = (self.keybind_help_window.show_window
or self.prefs_file_window.show_window
or self.user_guide_window.show_window
or self.quit_dialog.show_dialog
or self.command_runner.show_dialog)
if self.app_help_text:
floating_window_is_open = (self.app_help_window.show_window
or floating_window_is_open)
floating_plugin_is_open = any(
plugin.show_pane for plugin in self.floating_window_plugins)
return floating_window_is_open or floating_plugin_is_open
def exit_console(self):
"""Quit the console prompt_toolkit application UI."""
self.application.exit()
def logs_redraw(self):
emit_time = time.time()
# Has enough time passed since last UI redraw due to new logs?
if emit_time > self._last_ui_update_time + self.log_ui_update_frequency:
# Update last log time
self._last_ui_update_time = emit_time
# Trigger Prompt Toolkit UI redraw.
self.redraw_ui()
def redraw_ui(self):
"""Redraw the prompt_toolkit UI."""
if hasattr(self, 'application'):
# Thread safe way of sending a repaint trigger to the input event
# loop.
self.application.invalidate()
def setup_command_runner_log_pane(self) -> None:
if not self.system_command_output_pane is None:
return
self.system_command_output_pane = LogPane(application=self,
pane_title='Shell Output')
self.system_command_output_pane.add_log_handler(_SYSTEM_COMMAND_LOG,
level_name='INFO')
self.system_command_output_pane.log_view.log_store.formatter = (
logging.Formatter('%(message)s'))
self.system_command_output_pane.table_view = False
self.system_command_output_pane.show_pane = True
# Enable line wrapping
self.system_command_output_pane.toggle_wrap_lines()
# Blank right side toolbar text
self.system_command_output_pane._pane_subtitle = ' ' # pylint: disable=protected-access
self.window_manager.add_pane(self.system_command_output_pane)
async def run(self, test_mode=False):
"""Start the prompt_toolkit UI."""
if test_mode:
background_log_task = start_fake_logger(
lines=self.user_guide_window.help_text_area.document.lines,
log_thread_entry=self._test_mode_log_thread_entry,
log_thread_loop=self.test_mode_log_loop)
# Repl pane has focus by default, if it's hidden switch focus to another
# visible pane.
if not self.repl_pane.show_pane:
self.window_manager.focus_first_visible_pane()
try:
unused_result = await self.application.run_async(
set_exception_handler=True)
finally:
if test_mode:
background_log_task.cancel()
# TODO(tonymd): Remove this alias when not used by downstream projects.
def embed(
*args,
**kwargs,
) -> None:
"""PwConsoleEmbed().embed() alias."""
# Import here to avoid circular dependency
from pw_console.embed import PwConsoleEmbed # pylint: disable=import-outside-toplevel
console = PwConsoleEmbed(*args, **kwargs)
console.embed()