xref: /aosp_15_r20/external/pigweed/pw_console/py/pw_console/plugins/calc_pane.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Example text input-output Plugin."""
15
16from __future__ import annotations
17
18from typing import TYPE_CHECKING
19
20from prompt_toolkit.document import Document
21from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
22from prompt_toolkit.layout import Window
23from prompt_toolkit.widgets import SearchToolbar, TextArea
24
25from pw_console.widgets import ToolbarButton, WindowPane, WindowPaneToolbar
26
27if TYPE_CHECKING:
28    from pw_console.console_app import ConsoleApp
29
30
31class CalcPane(WindowPane):
32    """Example plugin that accepts text input and displays output.
33
34    This plugin is similar to the full-screen calculator example provided in
35    prompt_toolkit:
36    https://github.com/prompt-toolkit/python-prompt-toolkit/blob/3.0.23/examples/full-screen/calculator.py
37
38    It's a full window that can be moved around the user interface like other
39    Pigweed Console window panes. An input prompt is displayed on the bottom of
40    the window where the user can type in some math equation. When the enter key
41    is pressed the input is processed and the result shown in the top half of
42    the window.
43
44    Both input and output fields are prompt_toolkit TextArea objects which can
45    have their own options like syntax highlighting.
46    """
47
48    def __init__(self):
49        # Call WindowPane.__init__ and set the title to 'Calculator'
50        super().__init__(pane_title='Calculator')
51
52        # Create a TextArea for the output-field
53        # TextArea is a prompt_toolkit widget that can display editable text in
54        # a buffer. See the prompt_toolkit docs for all possible options:
55        # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.widgets.TextArea
56        self.output_field = TextArea(
57            # Optional Styles to apply to this TextArea
58            style='class:output-field',
59            # Initial text to put into the buffer.
60            text='Calculator Output',
61            # Allow this buffer to be in focus. This lets you drag select text
62            # contained inside, and edit the contents unless readonly.
63            focusable=True,
64            # Focus on mouse click.
65            focus_on_click=True,
66        )
67
68        # This is the search toolbar and only appears if the user presses ctrl-r
69        # to do reverse history search (similar to bash or zsh). Its used by the
70        # input_field below.
71        self.search_field = SearchToolbar()
72
73        # Create a TextArea for the user input.
74        self.input_field = TextArea(
75            # The height is set to 1 line
76            height=1,
77            # Prompt string that appears before the cursor.
78            prompt='>>> ',
79            # Optional Styles to apply to this TextArea
80            style='class:input-field',
81            # We only allow one line input for this example but multiline is
82            # supported by prompt_toolkit.
83            multiline=False,
84            wrap_lines=False,
85            # Allow reverse history search
86            search_field=self.search_field,
87            # Allow this input to be focused.
88            focusable=True,
89            # Focus on mouse click.
90            focus_on_click=True,
91        )
92
93        # The TextArea accept_handler function is called by prompt_toolkit (the
94        # UI) when the user presses enter. Here we override it to our own accept
95        # handler defined in this CalcPane class.
96        self.input_field.accept_handler = self.accept_input
97
98        # Create a toolbar for display at the bottom of this window. It will
99        # show the window title and toolbar buttons.
100        self.bottom_toolbar = WindowPaneToolbar(self)
101        self.bottom_toolbar.add_button(
102            ToolbarButton(
103                key='Enter',  # Key binding for this function
104                description='Run Calculation',  # Button name
105                # Function to run when clicked.
106                mouse_handler=self.run_calculation,
107            )
108        )
109        self.bottom_toolbar.add_button(
110            ToolbarButton(
111                key='Ctrl-c',  # Key binding for this function
112                description='Copy Output',  # Button name
113                # Function to run when clicked.
114                mouse_handler=self.copy_all_output,
115            )
116        )
117
118        # self.container is the root container that contains objects to be
119        # rendered in the UI, one on top of the other.
120        self.container = self._create_pane_container(
121            # Show the output_field on top
122            self.output_field,
123            # Draw a separator line with height=1
124            Window(height=1, char='─', style='class:line'),
125            # Show the input field just below that.
126            self.input_field,
127            # If ctrl-r reverse history is active, show the search box below the
128            # input_field.
129            self.search_field,
130            # Lastly, show the toolbar.
131            self.bottom_toolbar,
132        )
133
134    def pw_console_init(self, app: ConsoleApp) -> None:
135        """Set the Pigweed Console application instance.
136
137        This function is called after the Pigweed Console starts up and allows
138        access to the user preferences. Prefs is required for creating new
139        user-remappable keybinds."""
140        self.application = app
141        self.set_custom_keybinds()
142
143    def set_custom_keybinds(self) -> None:
144        # Fetch ConsoleApp preferences to load user keybindings
145        prefs = self.application.prefs
146        # Register a named keybind function that is user re-mappable
147        prefs.register_named_key_function(
148            'calc-pane.copy-selected-text',
149            # default bindings
150            ['c-c'],
151        )
152
153        # For setting additional keybindings to the output_field.
154        key_bindings = KeyBindings()
155
156        # Map the 'calc-pane.copy-selected-text' function keybind to the
157        # _copy_all_output function below. This will set
158        @prefs.register_keybinding('calc-pane.copy-selected-text', key_bindings)
159        def _copy_all_output(_event: KeyPressEvent) -> None:
160            """Copy selected text from the output buffer."""
161            self.copy_selected_output()
162
163        # Set the output_field controls key_bindings to the new bindings.
164        self.output_field.control.key_bindings = key_bindings
165
166    def run_calculation(self):
167        """Trigger the input_field's accept_handler.
168
169        This has the same effect as pressing enter in the input_field.
170        """
171        self.input_field.buffer.validate_and_handle()
172
173    def accept_input(self, _buffer):
174        """Function run when the user presses enter in the input_field.
175
176        Takes a buffer argument that contains the user's input text.
177        """
178        # Evaluate the user's calculator expression as Python and format the
179        # output result.
180        try:
181            output = "\n\nIn:  {}\nOut: {}".format(
182                self.input_field.text,
183                # NOTE: Don't use 'eval' in real code (this is just an example)
184                eval(self.input_field.text),  # pylint: disable=eval-used
185            )
186        except BaseException as exception:  # pylint: disable=broad-except
187            output = "\n\n{}".format(exception)
188
189        # Append the new output result to the existing output_field contents.
190        new_text = self.output_field.text + output
191
192        # Update the output_field with the new contents and move the
193        # cursor_position to the end.
194        self.output_field.buffer.document = Document(
195            text=new_text, cursor_position=len(new_text)
196        )
197
198    def copy_selected_output(self):
199        """Copy highlighted text in the output_field to the system clipboard."""
200        clipboard_data = self.output_field.buffer.copy_selection()
201        self.application.set_system_clipboard_data(clipboard_data)
202
203    def copy_all_output(self):
204        """Copy all text in the output_field to the system clipboard."""
205        self.application.set_system_clipboard(self.output_field.buffer.text)
206