blob: 884c1db227f9806e856f00baaffccf7db54701b4 [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.
"""pw_console embed class."""
import asyncio
import logging
from pathlib import Path
from typing import Dict, List, Iterable, Optional, Union
from prompt_toolkit.completion import WordCompleter
from pw_console.console_app import ConsoleApp
from pw_console.get_pw_console_app import PW_CONSOLE_APP_CONTEXTVAR
from pw_console.plugin_mixin import PluginMixin
import pw_console.python_logging
from pw_console.widgets import WindowPane, WindowPaneToolbar
class PwConsoleEmbed:
"""Embed class for customizing the console before startup."""
# pylint: disable=too-many-instance-attributes
def __init__(self,
global_vars=None,
local_vars=None,
loggers: Optional[Union[Dict[str, Iterable[logging.Logger]],
Iterable]] = None,
test_mode=False,
repl_startup_message: Optional[str] = None,
help_text: Optional[str] = None,
app_title: Optional[str] = None,
config_file_path: Optional[Union[str, Path]] = None) -> None:
"""Call this to embed pw console at the call point within your program.
Example usage: ::
import logging
from pw_console import PwConsoleEmbed
# Create the pw_console embed instance
console = PwConsoleEmbed(
global_vars=globals(),
local_vars=locals(),
loggers={
'Host Logs': [
logging.getLogger(__package__),
logging.getLogger(__file__)
],
'Device Logs': [
logging.getLogger('usb_gadget')
],
},
app_title='My Awesome Console',
config_file_path='/home/user/project/.pw_console.yaml',
)
# Optional: Add custom completions
console.add_sentence_completer(
{
'some_function', 'Function',
'some_variable', 'Variable',
}
)
# Setup Python loggers to output to a file instead of STDOUT.
console.setup_python_logging()
# Then run the console with:
console.embed()
Args:
global_vars: Dictionary representing the desired global symbol
table. Similar to what is returned by `globals()`.
local_vars: Dictionary representing the desired local symbol
table. Similar to what is returned by `locals()`.
loggers: Dict with keys of log window titles and values of
`logging.getLogger()` instances in lists. Each key that should
be shown in the pw console user interface.
app_title: Custom title text displayed in the user interface.
repl_startup_message: Custom text shown by default in the repl
output pane.
help_text: Custom text shown at the top of the help window before
keyboard shortcuts.
config_file_path: Path to a pw_console yaml config file.
"""
self.global_vars = global_vars
self.local_vars = local_vars
self.loggers = loggers
self.test_mode = test_mode
self.repl_startup_message = repl_startup_message
self.help_text = help_text
self.app_title = app_title
self.config_file_path = Path(
config_file_path) if config_file_path else None
self.console_app = None
self.extra_completers: List = []
self.setup_python_logging_called = False
self.hidden_by_default_windows: List[str] = []
self.window_plugins: List[WindowPane] = []
self.top_toolbar_plugins: List[WindowPaneToolbar] = []
self.bottom_toolbar_plugins: List[WindowPaneToolbar] = []
def add_window_plugin(self, window_pane: WindowPane) -> None:
"""Include a custom window pane plugin.
Args:
window_pane: Any instance of the WindowPane class.
"""
self.window_plugins.append(window_pane)
def add_top_toolbar(self, toolbar: WindowPaneToolbar) -> None:
"""Include a toolbar plugin to display on the top of the screen.
Top toolbars appear above all window panes and just below the main menu
bar. They span the full width of the screen.
Args:
toolbar: Instance of the WindowPaneToolbar class.
"""
self.top_toolbar_plugins.append(toolbar)
def add_bottom_toolbar(self, toolbar: WindowPaneToolbar) -> None:
"""Include a toolbar plugin to display at the bottom of the screen.
Bottom toolbars appear below all window panes and span the full width of
the screen.
Args:
toolbar: Instance of the WindowPaneToolbar class.
"""
self.bottom_toolbar_plugins.append(toolbar)
def add_sentence_completer(self,
word_meta_dict: Dict[str, str],
ignore_case=True):
"""Include a custom completer that matches on the entire repl input.
Args:
word_meta_dict: Dictionary representing the sentence completions
and descriptions. Keys are completion text, values are
descriptions.
"""
# Don't modify completion if empty.
if len(word_meta_dict) == 0:
return
sentences: List[str] = list(word_meta_dict.keys())
word_completer = WordCompleter(
sentences,
meta_dict=word_meta_dict,
ignore_case=ignore_case,
# Whole input field should match
sentence=True,
)
self.extra_completers.append(word_completer)
def _setup_log_panes(self):
"""Add loggers to ConsoleApp log pane(s)."""
if not self.loggers:
return
if isinstance(self.loggers, list):
self.console_app.add_log_handler('Logs', self.loggers)
elif isinstance(self.loggers, dict):
for window_title, logger_instances in self.loggers.items():
window_pane = self.console_app.add_log_handler(
window_title, logger_instances)
if window_pane.pane_title() in self.hidden_by_default_windows:
window_pane.show_pane = False
def setup_python_logging(self, last_resort_filename: Optional[str] = None):
"""Disable log handlers for full screen prompt_toolkit applications.
Args:
last_resort_filename: If specified use this file as a fallback for
unhandled python logging messages. Normally Python will output
any log messages with no handlers to STDERR as a fallback. If
none, a temp file will be created instead.
"""
self.setup_python_logging_called = True
pw_console.python_logging.setup_python_logging(last_resort_filename)
def hide_windows(self, *window_titles):
"""Hide window panes specified by title on console startup."""
for window_title in window_titles:
self.hidden_by_default_windows.append(window_title)
def embed(self):
"""Start the console."""
# Create the ConsoleApp instance.
self.console_app = ConsoleApp(
global_vars=self.global_vars,
local_vars=self.local_vars,
repl_startup_message=self.repl_startup_message,
help_text=self.help_text,
app_title=self.app_title,
extra_completers=self.extra_completers,
)
PW_CONSOLE_APP_CONTEXTVAR.set(self.console_app)
# Setup Python logging and log panes.
if not self.setup_python_logging_called:
self.setup_python_logging()
self._setup_log_panes()
# Add window pane plugins to the layout.
for window_pane in self.window_plugins:
window_pane.application = self.console_app
self.console_app.window_manager.add_pane(window_pane)
# Add toolbar plugins to the layout.
for toolbar in self.top_toolbar_plugins:
toolbar.application = self.console_app
self.console_app.window_manager.add_top_toolbar(toolbar)
for toolbar in self.bottom_toolbar_plugins:
toolbar.application = self.console_app
self.console_app.window_manager.add_bottom_toolbar(toolbar)
# Rebuild prompt_toolkit containers, menu items, and help content with
# any new plugins added above.
self.console_app.refresh_layout()
# Load external config if passed in.
if self.config_file_path:
self.console_app.load_clean_config(self.config_file_path)
self.console_app.apply_window_config()
# Start a thread for running user code.
self.console_app.start_user_code_thread()
# Startup any background threads and tasks required by plugins.
for window_pane in self.window_plugins:
if isinstance(window_pane, PluginMixin):
window_pane.plugin_start()
for toolbar in self.bottom_toolbar_plugins:
if isinstance(toolbar, PluginMixin):
toolbar.plugin_start()
for toolbar in self.top_toolbar_plugins:
if isinstance(toolbar, PluginMixin):
toolbar.plugin_start()
# Start the prompt_toolkit UI app.
asyncio.run(self.console_app.run(test_mode=self.test_mode),
debug=self.test_mode)