• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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