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