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