• 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
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