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