blob: 5d301ca1a19ad6e4e64dadc9ad178af40a7192c6 [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.
"""ReplPane class."""
import logging
from functools import partial
from typing import (
Any,
Callable,
Dict,
List,
Optional,
)
from prompt_toolkit.filters import (
Condition,
has_focus,
)
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
from prompt_toolkit.layout.dimension import AnyDimension
from prompt_toolkit.widgets import TextArea
from prompt_toolkit.layout import (
ConditionalContainer,
Dimension,
Float,
FloatContainer,
FormattedTextControl,
HSplit,
VSplit,
Window,
WindowAlign,
)
from prompt_toolkit.lexers import PygmentsLexer # type: ignore
from pygments.lexers.python import PythonLexer # type: ignore
_LOG = logging.getLogger(__package__)
_Namespace = Dict[str, Any]
_GetNamespace = Callable[[], _Namespace]
def mouse_focus_handler(repl_pane, mouse_event: MouseEvent):
"""Focus the repl_pane on click."""
if not has_focus(repl_pane)():
if mouse_event.event_type == MouseEventType.MOUSE_UP:
repl_pane.application.application.layout.focus(repl_pane)
return None
return NotImplemented
class FocusOnClickFloatContainer(ConditionalContainer):
"""Empty container rendered if the repl_pane is not in focus.
This container should be rendered with transparent=True so nothing is shown
to the user. Container is not rendered if the repl_pane is already in focus.
"""
def __init__(self, repl_pane):
empty_text = FormattedTextControl([(
# Style
'',
# Text
' ',
# Mouse handler
partial(mouse_focus_handler, repl_pane),
)])
super().__init__(
Window(empty_text),
filter=Condition(lambda: not has_focus(repl_pane)()),
)
class ReplPaneBottomToolbarBar(ConditionalContainer):
"""Repl pane bottom toolbar."""
@staticmethod
def get_center_text_tokens(repl_pane):
"""Return toolbar text showing if the ReplPane is in focus or not."""
focused_text = (
# Style
"",
# Text
" [FOCUSED] ",
# Mouse handler
partial(mouse_focus_handler, repl_pane),
)
out_of_focus_text = (
# Style
'class:keyhelp',
# Text
' [click to focus] ',
# Mouse handler
partial(mouse_focus_handler, repl_pane),
)
if has_focus(repl_pane)():
return [focused_text]
return [out_of_focus_text]
def __init__(self, repl_pane):
left_section_text = FormattedTextControl([(
# Style
'class:logo',
# Text
' Python Input ',
# Mouse handler
partial(mouse_focus_handler, repl_pane),
)])
center_section_text = FormattedTextControl(
# Callable to get formatted text tuples.
partial(ReplPaneBottomToolbarBar.get_center_text_tokens,
repl_pane))
right_section_text = FormattedTextControl([(
# Style
'class:bottom_toolbar_colored_text',
# Text
' [Enter]: run code ',
)])
left_section_window = Window(
content=left_section_text,
align=WindowAlign.LEFT,
dont_extend_width=True,
)
center_section_window = Window(
content=center_section_text,
# Center text is left justified to appear just right of the left
# section text.
align=WindowAlign.LEFT,
# Expand center section to fill space between the left and right
# side of the toolbar.
dont_extend_width=False,
)
right_section_window = Window(
content=right_section_text,
# Right side text should appear at the far right of the toolbar
align=WindowAlign.RIGHT,
dont_extend_width=True,
)
toolbar_vsplit = VSplit(
[
left_section_window,
center_section_window,
right_section_window,
],
height=1,
style='class:bottom_toolbar',
align=WindowAlign.LEFT,
)
super().__init__(
# Content
toolbar_vsplit,
filter=Condition(lambda: repl_pane.show_bottom_toolbar))
class ReplPane:
"""Pane for reading Python input."""
# pylint: disable=too-many-instance-attributes,too-few-public-methods
def __init__(
self,
application: Any,
# TODO: Include ptpython repl.
# python_repl: PwPtPythonRepl,
# TODO: Make the height of input+output windows match the log pane
# height. (Using minimum output height of 5 for now).
output_height: Optional[AnyDimension] = Dimension(preferred=5),
# TODO: Figure out how to resize ptpython input field.
_input_height: Optional[AnyDimension] = None,
# Default width and height to 50% of the screen
height: Optional[AnyDimension] = Dimension(weight=50),
width: Optional[AnyDimension] = Dimension(weight=50),
) -> None:
self.height = height
self.width = width
self.executed_code: List = []
self.application = application
self.show_top_toolbar = True
self.show_bottom_toolbar = True
# TODO: Include ptpython repl.
self.pw_ptpython_repl = Window(content=FormattedTextControl(
[('', '>>> Repl input buffer')], focusable=True))
self.last_error_output = ""
self.output_field = TextArea(
text='Repl output buffer',
style='class:output-field',
height=output_height,
# text=help_text,
focusable=False,
scrollbar=True,
lexer=PygmentsLexer(PythonLexer),
)
self.bottom_toolbar = ReplPaneBottomToolbarBar(self)
# ReplPane root container
self.container = FloatContainer(
# Horizontal split of all Repl pane sections.
HSplit(
[
# 1. Repl Output
self.output_field,
# 2. Static separator toolbar.
Window(
content=FormattedTextControl([(
# Style
'class:logo',
# Text
' Python Results ',
)]),
height=1,
style='class:menu-bar'),
# 3. Repl Input
self.pw_ptpython_repl,
# 4. Bottom toolbar
self.bottom_toolbar,
],
height=self.height,
width=self.width,
),
floats=[
# Transparent float container that will focus on the repl_pane
# when clicked. It is hidden if already in focus.
Float(
FocusOnClickFloatContainer(self),
transparent=True,
# Full size of the ReplPane minus one line for the bottom
# toolbar.
right=0,
left=0,
top=0,
bottom=1,
),
])
def __pt_container__(self):
"""Return the prompt_toolkit container for this ReplPane."""
return self.container
# pylint: disable=no-self-use
def get_all_key_bindings(self) -> List:
"""Return all keybinds for this plugin."""
return []
def after_render_hook(self):
"""Run tasks after the last UI render."""