xref: /aosp_15_r20/external/pigweed/pw_console/py/pw_console/plugins/clock_pane.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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