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 Plugin that displays some dynamic content (a clock) and examples of 15text formatting.""" 16 17from datetime import datetime 18 19from prompt_toolkit.filters import Condition, has_focus 20from prompt_toolkit.formatted_text import ( 21 FormattedText, 22 HTML, 23 merge_formatted_text, 24) 25from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent 26from prompt_toolkit.layout import FormattedTextControl, Window, WindowAlign 27from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 28 29from pw_console.plugin_mixin import PluginMixin 30from pw_console.widgets import ToolbarButton, WindowPane, WindowPaneToolbar 31from pw_console.get_pw_console_app import get_pw_console_app 32 33# Helper class used by the ClockPane plugin for displaying dynamic text, 34# handling key bindings and mouse input. See the ClockPane class below for the 35# beginning of the plugin implementation. 36 37 38class ClockControl(FormattedTextControl): 39 """Example prompt_toolkit UIControl for displaying formatted text. 40 41 This is the prompt_toolkit class that is responsible for drawing the clock, 42 handling keybindings if in focus, and mouse input. 43 """ 44 def __init__(self, clock_pane: 'ClockPane', *args, **kwargs) -> None: 45 self.clock_pane = clock_pane 46 47 # Set some custom key bindings to toggle the view mode and wrap lines. 48 key_bindings = KeyBindings() 49 50 # If you press the v key this _toggle_view_mode function will be run. 51 @key_bindings.add('v') 52 def _toggle_view_mode(_event: KeyPressEvent) -> None: 53 """Toggle view mode.""" 54 self.clock_pane.toggle_view_mode() 55 56 # If you press the w key this _toggle_wrap_lines function will be run. 57 @key_bindings.add('w') 58 def _toggle_wrap_lines(_event: KeyPressEvent) -> None: 59 """Toggle line wrapping.""" 60 self.clock_pane.toggle_wrap_lines() 61 62 # Include the key_bindings keyword arg when passing to the parent class 63 # __init__ function. 64 kwargs['key_bindings'] = key_bindings 65 # Call the parent FormattedTextControl.__init__ 66 super().__init__(*args, **kwargs) 67 68 def mouse_handler(self, mouse_event: MouseEvent): 69 """Mouse handler for this control.""" 70 # If the user clicks anywhere this function is run. 71 72 # Mouse positions relative to this control. x is the column starting 73 # from the left size as zero. y is the row starting with the top as 74 # zero. 75 _click_x = mouse_event.position.x 76 _click_y = mouse_event.position.y 77 78 # Mouse click behavior usually depends on if this window pane is in 79 # focus. If not in focus, then focus on it when left clicking. If 80 # already in focus then perform the action specific to this window. 81 82 # If not in focus, change focus to this clock pane and do nothing else. 83 if not has_focus(self.clock_pane)(): 84 if mouse_event.event_type == MouseEventType.MOUSE_UP: 85 get_pw_console_app().focus_on_container(self.clock_pane) 86 # Mouse event handled, return None. 87 return None 88 89 # If code reaches this point, this window is already in focus. 90 # On left click 91 if mouse_event.event_type == MouseEventType.MOUSE_UP: 92 # Toggle the view mode. 93 self.clock_pane.toggle_view_mode() 94 # Mouse event handled, return None. 95 return None 96 97 # Mouse event not handled, return NotImplemented. 98 return NotImplemented 99 100 101class ClockPane(WindowPane, PluginMixin): 102 """Example Pigweed Console plugin window that displays a clock. 103 104 The ClockPane is a WindowPane based plugin that displays a clock and some 105 formatted text examples. It inherits from both WindowPane and 106 PluginMixin. It can be added on console startup by calling: :: 107 108 my_console.add_window_plugin(ClockPane()) 109 110 For an example see: 111 https://pigweed.dev/pw_console/embedding.html#adding-plugins 112 """ 113 def __init__(self, *args, **kwargs): 114 super().__init__(*args, pane_title='Clock', **kwargs) 115 # Some toggle settings to change view and wrap lines. 116 self.view_mode_clock: bool = True 117 self.wrap_lines: bool = False 118 # Counter variable to track how many times the background task runs. 119 self.background_task_update_count: int = 0 120 121 # ClockControl is responsible for rendering the dynamic content provided 122 # by self._get_formatted_text() and handle keyboard and mouse input. 123 # Using a control is always necessary for displaying any content that 124 # will change. 125 self.clock_control = ClockControl( 126 self, # This ClockPane class 127 self._get_formatted_text, # Callable to get text for display 128 # These are FormattedTextControl options. 129 # See the prompt_toolkit docs for all possible options 130 # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.layout.FormattedTextControl 131 show_cursor=False, 132 focusable=True, 133 ) 134 135 # Every FormattedTextControl object (ClockControl) needs to live inside 136 # a prompt_toolkit Window() instance. Here is where you specify 137 # alignment, style, and dimensions. See the prompt_toolkit docs for all 138 # opitons: 139 # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.layout.Window 140 self.clock_control_window = Window( 141 # Set the content to the clock_control defined above. 142 content=self.clock_control, 143 # Make content left aligned 144 align=WindowAlign.LEFT, 145 # These two set to false make this window fill all available space. 146 dont_extend_width=False, 147 dont_extend_height=False, 148 # Content inside this window will have its lines wrapped if 149 # self.wrap_lines is True. 150 wrap_lines=Condition(lambda: self.wrap_lines), 151 ) 152 153 # Create a toolbar for display at the bottom of this clock window. It 154 # will show the window title and buttons. 155 self.bottom_toolbar = WindowPaneToolbar(self) 156 157 # Add a button to toggle the view mode. 158 self.bottom_toolbar.add_button( 159 ToolbarButton( 160 key='v', # Key binding for this function 161 description='View Mode', # Button name 162 # Function to run when clicked. 163 mouse_handler=self.toggle_view_mode, 164 )) 165 166 # Add a checkbox button to display if wrap_lines is enabled. 167 self.bottom_toolbar.add_button( 168 ToolbarButton( 169 key='w', # Key binding for this function 170 description='Wrap', # Button name 171 # Function to run when clicked. 172 mouse_handler=self.toggle_wrap_lines, 173 # Display a checkbox in this button. 174 is_checkbox=True, 175 # lambda that returns the state of the checkbox 176 checked=lambda: self.wrap_lines, 177 )) 178 179 # self.container is the root container that contains objects to be 180 # rendered in the UI, one on top of the other. 181 self.container = self._create_pane_container( 182 # Display the clock window on top... 183 self.clock_control_window, 184 # and the bottom_toolbar below. 185 self.bottom_toolbar, 186 ) 187 188 # This plugin needs to run a task in the background periodically and 189 # uses self.plugin_init() to set which function to run, and how often. 190 # This is provided by PluginMixin. See the docs for more info: 191 # https://pigweed.dev/pw_console/plugins.html#background-tasks 192 self.plugin_init( 193 plugin_callback=self._background_task, 194 # Run self._background_task once per second. 195 plugin_callback_frequency=1.0, 196 plugin_logger_name='pw_console_example_clock_plugin', 197 ) 198 199 def _background_task(self) -> bool: 200 """Function run in the background for the ClockPane plugin.""" 201 self.background_task_update_count += 1 202 # Make a log message for debugging purposes. For more info see: 203 # https://pigweed.dev/pw_console/plugins.html#debugging-plugin-behavior 204 self.plugin_logger.debug('background_task_update_count: %s', 205 self.background_task_update_count) 206 207 # Returning True in the background task will force the user interface to 208 # re-draw. 209 # Returning False means no updates required. 210 return True 211 212 def toggle_view_mode(self): 213 """Toggle the view mode between the clock and formatted text example.""" 214 self.view_mode_clock = not self.view_mode_clock 215 self.redraw_ui() 216 217 def toggle_wrap_lines(self): 218 """Enable or disable line wraping/truncation.""" 219 self.wrap_lines = not self.wrap_lines 220 self.redraw_ui() 221 222 def _get_formatted_text(self): 223 """This function returns the content that will be displayed in the user 224 interface depending on which view mode is active.""" 225 if self.view_mode_clock: 226 return self._get_clock_text() 227 return self._get_example_text() 228 229 def _get_clock_text(self): 230 """Create the time with some color formatting.""" 231 # pylint: disable=no-self-use 232 233 # Get the date and time 234 date, time = datetime.now().isoformat(sep='_', 235 timespec='seconds').split('_') 236 237 # Formatted text is represented as (style, text) tuples. 238 # For more examples see: 239 # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/printing_text.html 240 241 # These styles are selected using class names and start with the 242 # 'class:' prefix. For all classes defined by Pigweed Console see: 243 # https://cs.opensource.google/pigweed/pigweed/+/main:pw_console/py/pw_console/style.py;l=189 244 245 # Date in cyan matching the current Pigweed Console theme. 246 date_with_color = ('class:theme-fg-cyan', date) 247 # Time in magenta 248 time_with_color = ('class:theme-fg-magenta', time) 249 250 # No color styles for line breaks and spaces. 251 line_break = ('', '\n') 252 space = ('', ' ') 253 254 # Concatenate the (style, text) tuples. 255 return FormattedText([ 256 line_break, 257 space, 258 space, 259 date_with_color, 260 space, 261 time_with_color, 262 ]) 263 264 def _get_example_text(self): 265 """Examples of how to create formatted text.""" 266 # pylint: disable=no-self-use 267 # Make a list to hold all the formatted text to display. 268 fragments = [] 269 270 # Some spacing vars 271 wide_space = ('', ' ') 272 space = ('', ' ') 273 newline = ('', '\n') 274 275 # HTML() is a shorthand way to style text. See: 276 # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/printing_text.html#html 277 # This formats 'Foreground Colors' as underlined: 278 fragments.append(HTML('<u>Foreground Colors</u>\n')) 279 280 # Standard ANSI colors examples 281 fragments.append( 282 FormattedText([ 283 # These tuples follow this format: 284 # (style_string, text_to_display) 285 ('ansiblack', 'ansiblack'), 286 wide_space, 287 ('ansired', 'ansired'), 288 wide_space, 289 ('ansigreen', 'ansigreen'), 290 wide_space, 291 ('ansiyellow', 'ansiyellow'), 292 wide_space, 293 ('ansiblue', 'ansiblue'), 294 wide_space, 295 ('ansimagenta', 'ansimagenta'), 296 wide_space, 297 ('ansicyan', 'ansicyan'), 298 wide_space, 299 ('ansigray', 'ansigray'), 300 wide_space, 301 newline, 302 ('ansibrightblack', 'ansibrightblack'), 303 space, 304 ('ansibrightred', 'ansibrightred'), 305 space, 306 ('ansibrightgreen', 'ansibrightgreen'), 307 space, 308 ('ansibrightyellow', 'ansibrightyellow'), 309 space, 310 ('ansibrightblue', 'ansibrightblue'), 311 space, 312 ('ansibrightmagenta', 'ansibrightmagenta'), 313 space, 314 ('ansibrightcyan', 'ansibrightcyan'), 315 space, 316 ('ansiwhite', 'ansiwhite'), 317 space, 318 ])) 319 320 fragments.append(HTML('\n<u>Background Colors</u>\n')) 321 fragments.append( 322 FormattedText([ 323 # Here's an example of a style that specifies both background 324 # and foreground colors. The background color is prefixed with 325 # 'bg:'. The foreground color follows that with no prefix. 326 ('bg:ansiblack ansiwhite', 'ansiblack'), 327 wide_space, 328 ('bg:ansired', 'ansired'), 329 wide_space, 330 ('bg:ansigreen', 'ansigreen'), 331 wide_space, 332 ('bg:ansiyellow', 'ansiyellow'), 333 wide_space, 334 ('bg:ansiblue ansiwhite', 'ansiblue'), 335 wide_space, 336 ('bg:ansimagenta', 'ansimagenta'), 337 wide_space, 338 ('bg:ansicyan', 'ansicyan'), 339 wide_space, 340 ('bg:ansigray', 'ansigray'), 341 wide_space, 342 ('', '\n'), 343 ('bg:ansibrightblack', 'ansibrightblack'), 344 space, 345 ('bg:ansibrightred', 'ansibrightred'), 346 space, 347 ('bg:ansibrightgreen', 'ansibrightgreen'), 348 space, 349 ('bg:ansibrightyellow', 'ansibrightyellow'), 350 space, 351 ('bg:ansibrightblue', 'ansibrightblue'), 352 space, 353 ('bg:ansibrightmagenta', 'ansibrightmagenta'), 354 space, 355 ('bg:ansibrightcyan', 'ansibrightcyan'), 356 space, 357 ('bg:ansiwhite', 'ansiwhite'), 358 space, 359 ])) 360 361 # These themes use Pigweed Console style classes. See full list in: 362 # https://cs.opensource.google/pigweed/pigweed/+/main:pw_console/py/pw_console/style.py;l=189 363 fragments.append(HTML('\n\n<u>Current Theme Foreground Colors</u>\n')) 364 fragments.append([ 365 ('class:theme-fg-red', 'class:theme-fg-red'), 366 newline, 367 ('class:theme-fg-orange', 'class:theme-fg-orange'), 368 newline, 369 ('class:theme-fg-yellow', 'class:theme-fg-yellow'), 370 newline, 371 ('class:theme-fg-green', 'class:theme-fg-green'), 372 newline, 373 ('class:theme-fg-cyan', 'class:theme-fg-cyan'), 374 newline, 375 ('class:theme-fg-blue', 'class:theme-fg-blue'), 376 newline, 377 ('class:theme-fg-purple', 'class:theme-fg-purple'), 378 newline, 379 ('class:theme-fg-magenta', 'class:theme-fg-magenta'), 380 newline, 381 ]) 382 383 fragments.append(HTML('\n<u>Current Theme Background Colors</u>\n')) 384 fragments.append([ 385 ('class:theme-bg-red', 'class:theme-bg-red'), 386 newline, 387 ('class:theme-bg-orange', 'class:theme-bg-orange'), 388 newline, 389 ('class:theme-bg-yellow', 'class:theme-bg-yellow'), 390 newline, 391 ('class:theme-bg-green', 'class:theme-bg-green'), 392 newline, 393 ('class:theme-bg-cyan', 'class:theme-bg-cyan'), 394 newline, 395 ('class:theme-bg-blue', 'class:theme-bg-blue'), 396 newline, 397 ('class:theme-bg-purple', 'class:theme-bg-purple'), 398 newline, 399 ('class:theme-bg-magenta', 'class:theme-bg-magenta'), 400 newline, 401 ]) 402 403 fragments.append(HTML('\n<u>Theme UI Colors</u>\n')) 404 fragments.append([ 405 ('class:theme-fg-default', 'class:theme-fg-default'), 406 space, 407 ('class:theme-bg-default', 'class:theme-bg-default'), 408 space, 409 ('class:theme-bg-active', 'class:theme-bg-active'), 410 space, 411 ('class:theme-fg-active', 'class:theme-fg-active'), 412 space, 413 ('class:theme-bg-inactive', 'class:theme-bg-inactive'), 414 space, 415 ('class:theme-fg-inactive', 'class:theme-fg-inactive'), 416 newline, 417 ('class:theme-fg-dim', 'class:theme-fg-dim'), 418 space, 419 ('class:theme-bg-dim', 'class:theme-bg-dim'), 420 space, 421 ('class:theme-bg-dialog', 'class:theme-bg-dialog'), 422 space, 423 ('class:theme-bg-line-highlight', 'class:theme-bg-line-highlight'), 424 space, 425 ('class:theme-bg-button-active', 'class:theme-bg-button-active'), 426 space, 427 ('class:theme-bg-button-inactive', 428 'class:theme-bg-button-inactive'), 429 space, 430 ]) 431 432 # Return all formatted text lists merged together. 433 return merge_formatted_text(fragments) 434