Plugin Guide#

Pigweed Console supports extending the user interface with custom widgets. For example: Toolbars that display device information and provide buttons for interacting with the device.

Writing Plugins#

Creating new plugins has a few high level steps:

  1. Create a new Python class inheriting from either WindowPane or WindowPaneToolbar.

    • Optionally inherit from The PluginMixin class as well for running background tasks.

  2. Enable the plugin before pw_console startup by calling add_window_plugin, add_floating_window_plugin, add_top_toolbar or add_bottom_toolbar. See the Adding Plugins section of the Embedding Guide for an example.

  3. Run the console and enjoy!

    • Debugging Plugin behavior can be done by logging to a dedicated Python logger and viewing in-app. See Debugging Plugin Behavior below.

Background Tasks#

Plugins may need to have long running background tasks which could block or slow down the Pigweed Console user interface. For those situations use the PluginMixin class. Plugins can inherit from this and setup the callback that should be executed in the background.

class pw_console.plugin_mixin.PluginMixin#

Bases: object

Handles background task management in a Pigweed Console plugin.

Pigweed Console plugins can inherit from this class if they require running tasks in the background. This is important as any plugin code not in its own dedicated thread can potentially block the user interface

Example usage:

import logging
from pw_console.plugin_mixin import PluginMixin
from pw_console.widgets import WindowPaneToolbar

class AwesomeToolbar(WindowPaneToolbar, PluginMixin):
    TOOLBAR_HEIGHT = 1

    def __init__(self, *args, **kwargs):
        # Call parent class WindowPaneToolbar.__init__
        super().__init__(*args, **kwargs)

        # Set PluginMixin to execute
        # self._awesome_background_task every 10 seconds.
        self.plugin_init(
            plugin_callback=self._awesome_background_task,
            plugin_callback_frequency=10.0,
            plugin_logger_name='awesome_toolbar_plugin')

    # This function will be run in a separate thread every 10 seconds.
    def _awesome_background_task(self) -> bool:
        time.sleep(1)  # Do real work here.

        if self.new_data_processed:
            # If new data was processed, and the user interface
            # should be updated return True.

            # Log using self.plugin_logger for debugging.
            self.plugin_logger.debug('New data processed')

            # Return True to signal a UI redraw.
            return True

        # Returning False means no updates needed.
        return False
plugin_callback#

Callable that is run in a background thread.

plugin_callback_frequency#

Number of seconds to wait between executing plugin_callback.

plugin_logger#

logging instance for this plugin. Useful for debugging code running in a separate thread.

plugin_callback_future#

Future object for the plugin background task.

plugin_event_loop#

asyncio event loop running in the background thread.

plugin_enable_background_task#

If True, keep periodically running plugin_callback at the desired frequency. If False the background task will stop.

plugin_init(plugin_callback: Optional[Callable[[...], bool]] = None, plugin_callback_frequency: float = 30.0, plugin_logger_name: Optional[str] = 'pw_console_plugins') None#

Call this on __init__() to set plugin background task variables.

Parameters
  • plugin_callback – Callable to run in a separate thread from the Pigweed Console UI. This function should return True if the UI should be redrawn after execution.

  • plugin_callback_frequency – Number of seconds to wait between executing plugin_callback.

  • plugin_logger_name – Unique name for this plugin’s Python logger. Useful for debugging code running in a separate thread.

plugin_start()#

Function used to start this plugin’s background thead and task.

Debugging Plugin Behavior#

If your plugin uses background threads for updating it can be difficult to see errors. Often, nothing will appear to be happening and exceptions may not be visible. When using PluginMixin you can specify a name for a Python logger to use with the plugin_logger_name keyword argument.

class AwesomeToolbar(WindowPaneToolbar, PluginMixin):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.update_count = 0

        self.plugin_init(
            plugin_callback=self._background_task,
            plugin_callback_frequency=1.0,
            plugin_logger_name='my_awesome_plugin',
        )

    def _background_task(self) -> bool:
        self.update_count += 1
        self.plugin_logger.debug('background_task_update_count: %s',
                                 self.update_count)
        return True

This will let you open up a new log window while the console is running to see what the plugin is doing. Open up the logger name provided above by clicking in the main menu: File > Open Logger > my_awesome_plugin.

Sample Plugins#

Pigweed Console will provide a few sample plugins to serve as templates for creating your own plugins. These are a work in progress at the moment and not available at this time.

Calculator#

This plugin is similar to the full-screen calculator.py example provided in prompt_toolkit. It’s a full window that can be moved around the user interface like other Pigweed Console window panes. An input prompt is displayed on the bottom of the window where the user can type in some math equation. When the enter key is pressed the input is processed and the result shown in the top half of the window.

Both input and output fields are prompt_toolkit TextArea objects which can have their own options like syntax highlighting.

Screenshot of the CalcPane plugin showing some math calculations.

Screenshot of the CalcPane plugin showing some math calculations.#

The code is heavily commented and describes what each line is doing. See the Code Listing: calc_pane.py for the full source.

Clock#

The ClockPane is another WindowPane based plugin that displays a clock and some formatted text examples. It inherits from both WindowPane and PluginMixin.

ClockPane plugin screenshot showing the clock text.

ClockPane plugin screenshot showing the clock text.#

This plugin makes use of PluginMixin to run a task a background thread that triggers UI re-draws. There are also two toolbar buttons to toggle view mode (between the clock and some sample text) and line wrapping. pressing the v key or mouse clicking on the View Mode button will toggle the view to show some formatted text samples:

ClockPane plugin screenshot showing formatted text examples.

ClockPane plugin screenshot showing formatted text examples.#

Like the CalcPane example the code is heavily commented to guide plugin authors through developmenp. See the Code Listing: clock_pane.py below for the full source.

2048 Game#

This is a plugin that demonstrates more complex user interaction by playing a game of 2048.

Similar to the ClockPane the Twenty48Pane class inherits from PluginMixin to manage background tasks. With a few differences:

  • Uses FloatingWindowPane to create a floating window instead of a standard tiled window.

  • Implements the get_top_level_menus function to create a new [2048] menu in Pigweed Console’s own main menu bar.

  • Adds custom game keybindings which are set within the Twenty48Control class. That is the prompt_toolkit FormattedTextControl widget which receives keyboard input when the game is in focus.

The Twenty48Game class is separate from the user interface and handles managing the game state as well as printing the game board. The Twenty48Game.__pt_formatted_text__() function is responsible for drawing the game board using prompt_toolkit style and text tuples.

Twenty48Pane plugin screenshot showing the game board.

Twenty48Pane plugin screenshot showing the game board.#

Appendix#

Code Listing: calc_pane.py#

  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 text input-output Plugin."""
 15
 16from typing import TYPE_CHECKING
 17
 18from prompt_toolkit.document import Document
 19from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
 20from prompt_toolkit.layout import Window
 21from prompt_toolkit.widgets import SearchToolbar, TextArea
 22
 23from pw_console.widgets import ToolbarButton, WindowPane, WindowPaneToolbar
 24
 25if TYPE_CHECKING:
 26    from pw_console.console_app import ConsoleApp
 27
 28
 29class CalcPane(WindowPane):
 30    """Example plugin that accepts text input and displays output.
 31
 32    This plugin is similar to the full-screen calculator example provided in
 33    prompt_toolkit:
 34    https://github.com/prompt-toolkit/python-prompt-toolkit/blob/3.0.23/examples/full-screen/calculator.py
 35
 36    It's a full window that can be moved around the user interface like other
 37    Pigweed Console window panes. An input prompt is displayed on the bottom of
 38    the window where the user can type in some math equation. When the enter key
 39    is pressed the input is processed and the result shown in the top half of
 40    the window.
 41
 42    Both input and output fields are prompt_toolkit TextArea objects which can
 43    have their own options like syntax highlighting.
 44    """
 45    def __init__(self):
 46        # Call WindowPane.__init__ and set the title to 'Calculator'
 47        super().__init__(pane_title='Calculator')
 48
 49        # Create a TextArea for the output-field
 50        # TextArea is a prompt_toolkit widget that can display editable text in
 51        # a buffer. See the prompt_toolkit docs for all possible options:
 52        # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.widgets.TextArea
 53        self.output_field = TextArea(
 54            # Optional Styles to apply to this TextArea
 55            style='class:output-field',
 56            # Initial text to put into the buffer.
 57            text='Calculator Output',
 58            # Allow this buffer to be in focus. This lets you drag select text
 59            # contained inside, and edit the contents unless readonly.
 60            focusable=True,
 61            # Focus on mouse click.
 62            focus_on_click=True,
 63        )
 64
 65        # This is the search toolbar and only appears if the user presses ctrl-r
 66        # to do reverse history search (similar to bash or zsh). Its used by the
 67        # input_field below.
 68        self.search_field = SearchToolbar()
 69
 70        # Create a TextArea for the user input.
 71        self.input_field = TextArea(
 72            # The height is set to 1 line
 73            height=1,
 74            # Prompt string that appears before the cursor.
 75            prompt='>>> ',
 76            # Optional Styles to apply to this TextArea
 77            style='class:input-field',
 78            # We only allow one line input for this example but multiline is
 79            # supported by prompt_toolkit.
 80            multiline=False,
 81            wrap_lines=False,
 82            # Allow reverse history search
 83            search_field=self.search_field,
 84            # Allow this input to be focused.
 85            focusable=True,
 86            # Focus on mouse click.
 87            focus_on_click=True,
 88        )
 89
 90        # The TextArea accept_handler function is called by prompt_toolkit (the
 91        # UI) when the user presses enter. Here we override it to our own accept
 92        # handler defined in this CalcPane class.
 93        self.input_field.accept_handler = self.accept_input
 94
 95        # Create a toolbar for display at the bottom of this window. It will
 96        # show the window title and toolbar buttons.
 97        self.bottom_toolbar = WindowPaneToolbar(self)
 98        self.bottom_toolbar.add_button(
 99            ToolbarButton(
100                key='Enter',  # Key binding for this function
101                description='Run Calculation',  # Button name
102                # Function to run when clicked.
103                mouse_handler=self.run_calculation,
104            ))
105        self.bottom_toolbar.add_button(
106            ToolbarButton(
107                key='Ctrl-c',  # Key binding for this function
108                description='Copy Output',  # Button name
109                # Function to run when clicked.
110                mouse_handler=self.copy_all_output,
111            ))
112
113        # self.container is the root container that contains objects to be
114        # rendered in the UI, one on top of the other.
115        self.container = self._create_pane_container(
116            # Show the output_field on top
117            self.output_field,
118            # Draw a separator line with height=1
119            Window(height=1, char='─', style='class:line'),
120            # Show the input field just below that.
121            self.input_field,
122            # If ctrl-r reverse history is active, show the search box below the
123            # input_field.
124            self.search_field,
125            # Lastly, show the toolbar.
126            self.bottom_toolbar,
127        )
128
129    def pw_console_init(self, app: 'ConsoleApp') -> None:
130        """Set the Pigweed Console application instance.
131
132        This function is called after the Pigweed Console starts up and allows
133        access to the user preferences. Prefs is required for creating new
134        user-remappable keybinds."""
135        self.application = app
136        self.set_custom_keybinds()
137
138    def set_custom_keybinds(self) -> None:
139        # Fetch ConsoleApp preferences to load user keybindings
140        prefs = self.application.prefs
141        # Register a named keybind function that is user re-mappable
142        prefs.register_named_key_function(
143            'calc-pane.copy-selected-text',
144            # default bindings
145            ['c-c'])
146
147        # For setting additional keybindings to the output_field.
148        key_bindings = KeyBindings()
149
150        # Map the 'calc-pane.copy-selected-text' function keybind to the
151        # _copy_all_output function below. This will set
152        @prefs.register_keybinding('calc-pane.copy-selected-text',
153                                   key_bindings)
154        def _copy_all_output(_event: KeyPressEvent) -> None:
155            """Copy selected text from the output buffer."""
156            self.copy_selected_output()
157
158        # Set the output_field controls key_bindings to the new bindings.
159        self.output_field.control.key_bindings = key_bindings
160
161    def run_calculation(self):
162        """Trigger the input_field's accept_handler.
163
164        This has the same effect as pressing enter in the input_field.
165        """
166        self.input_field.buffer.validate_and_handle()
167
168    def accept_input(self, _buffer):
169        """Function run when the user presses enter in the input_field.
170
171        Takes a buffer argument that contains the user's input text.
172        """
173        # Evaluate the user's calculator expression as Python and format the
174        # output result.
175        try:
176            output = "\n\nIn:  {}\nOut: {}".format(
177                self.input_field.text,
178                # NOTE: Don't use 'eval' in real code (this is just an example)
179                eval(self.input_field.text))  # pylint: disable=eval-used
180        except BaseException as exception:  # pylint: disable=broad-except
181            output = "\n\n{}".format(exception)
182
183        # Append the new output result to the existing output_field contents.
184        new_text = self.output_field.text + output
185
186        # Update the output_field with the new contents and move the
187        # cursor_position to the end.
188        self.output_field.buffer.document = Document(
189            text=new_text, cursor_position=len(new_text))
190
191    def copy_selected_output(self):
192        """Copy highlighted text in the output_field to the system clipboard."""
193        clipboard_data = self.output_field.buffer.copy_selection()
194        self.application.application.clipboard.set_data(clipboard_data)
195
196    def copy_all_output(self):
197        """Copy all text in the output_field to the system clipboard."""
198        self.application.application.clipboard.set_text(
199            self.output_field.buffer.text)

Code Listing: clock_pane.py#

  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.pigweed.dev/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.pigweed.dev/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)

Code Listing: twenty48_pane.py#

  1# Copyright 2022 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 game of 2048."""
 15
 16from random import choice
 17from typing import Iterable, List, Tuple, TYPE_CHECKING
 18import time
 19
 20from prompt_toolkit.filters import has_focus
 21from prompt_toolkit.formatted_text import StyleAndTextTuples
 22from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
 23from prompt_toolkit.layout import (
 24    AnyContainer,
 25    Dimension,
 26    FormattedTextControl,
 27    HSplit,
 28    Window,
 29    WindowAlign,
 30    VSplit,
 31)
 32from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
 33from prompt_toolkit.widgets import MenuItem
 34
 35import pw_console.widgets.border
 36from pw_console.widgets import (
 37    FloatingWindowPane,
 38    ToolbarButton,
 39    WindowPaneToolbar,
 40)
 41from pw_console.plugin_mixin import PluginMixin
 42from pw_console.get_pw_console_app import get_pw_console_app
 43
 44if TYPE_CHECKING:
 45    from pw_console.console_app import ConsoleApp
 46
 47Twenty48Cell = Tuple[int, int, int]
 48
 49
 50class Twenty48Game:
 51    """2048 Game."""
 52    def __init__(self) -> None:
 53        self.colors = {
 54            2: 'bg:#dd6',
 55            4: 'bg:#da6',
 56            8: 'bg:#d86',
 57            16: 'bg:#d66',
 58            32: 'bg:#d6a',
 59            64: 'bg:#a6d',
 60            128: 'bg:#66d',
 61            256: 'bg:#68a',
 62            512: 'bg:#6a8',
 63            1024: 'bg:#6d6',
 64            2048: 'bg:#0f8',
 65            4096: 'bg:#0ff',
 66        }
 67        self.board: List[List[int]]
 68        self.last_board: List[Twenty48Cell]
 69        self.move_count: int
 70        self.width: int = 4
 71        self.height: int = 4
 72        self.max_value: int = 0
 73        self.start_time: float
 74        self.reset_game()
 75
 76    def reset_game(self) -> None:
 77        self.start_time = time.time()
 78        self.max_value = 2
 79        self.move_count = 0
 80        self.board = []
 81        for _i in range(self.height):
 82            self.board.append([0] * self.width)
 83        self.last_board = list(self.all_cells())
 84        self.add_random_tiles(2)
 85
 86    def stats(self) -> StyleAndTextTuples:
 87        """Returns stats on the game in progress."""
 88        elapsed_time = int(time.time() - self.start_time)
 89        minutes = int(elapsed_time / 60.0)
 90        seconds = elapsed_time % 60
 91        fragments: StyleAndTextTuples = []
 92        fragments.append(('', '\n'))
 93        fragments.append(('', f'Moves: {self.move_count}'))
 94        fragments.append(('', '\n'))
 95        fragments.append(('', 'Time:  {:0>2}:{:0>2}'.format(minutes, seconds)))
 96        fragments.append(('', '\n'))
 97        fragments.append(('', f'Max: {self.max_value}'))
 98        fragments.append(('', '\n\n'))
 99        fragments.append(('', 'Press R to restart\n'))
100        fragments.append(('', '\n'))
101        fragments.append(('', 'Arrow keys to move'))
102        return fragments
103
104    def __pt_formatted_text__(self) -> StyleAndTextTuples:
105        """Returns the game board formatted in a grid with colors."""
106        fragments: StyleAndTextTuples = []
107
108        def print_row(row: List[int], include_number: bool = False) -> None:
109            fragments.append(('', '  '))
110            for col in row:
111                style = 'class:theme-fg-default '
112                if col > 0:
113                    style = '#000 '
114                style += self.colors.get(col, '')
115                text = ' ' * 6
116                if include_number:
117                    text = '{:^6}'.format(col)
118                fragments.append((style, text))
119            fragments.append(('', '\n'))
120
121        fragments.append(('', '\n'))
122        for row in self.board:
123            print_row(row)
124            print_row(row, include_number=True)
125            print_row(row)
126
127        return fragments
128
129    def __repr__(self) -> str:
130        board = ''
131        for row_cells in self.board:
132            for column in row_cells:
133                board += '{:^6}'.format(column)
134            board += '\n'
135        return board
136
137    def all_cells(self) -> Iterable[Twenty48Cell]:
138        for row, row_cells in enumerate(self.board):
139            for col, cell_value in enumerate(row_cells):
140                yield (row, col, cell_value)
141
142    def update_max_value(self) -> None:
143        for _row, _col, value in self.all_cells():
144            if value > self.max_value:
145                self.max_value = value
146
147    def empty_cells(self) -> Iterable[Twenty48Cell]:
148        for row, row_cells in enumerate(self.board):
149            for col, cell_value in enumerate(row_cells):
150                if cell_value != 0:
151                    continue
152                yield (row, col, cell_value)
153
154    def _board_changed(self) -> bool:
155        return self.last_board != list(self.all_cells())
156
157    def complete_move(self) -> None:
158        if not self._board_changed():
159            # Move did nothing, ignore.
160            return
161
162        self.update_max_value()
163        self.move_count += 1
164        self.add_random_tiles()
165        self.last_board = list(self.all_cells())
166
167    def add_random_tiles(self, count: int = 1) -> None:
168        for _i in range(count):
169            empty_cells = list(self.empty_cells())
170            if not empty_cells:
171                return
172            row, col, _value = choice(empty_cells)
173            self.board[row][col] = 2
174
175    def row(self, row_index: int) -> Iterable[Twenty48Cell]:
176        for col, cell_value in enumerate(self.board[row_index]):
177            yield (row_index, col, cell_value)
178
179    def col(self, col_index: int) -> Iterable[Twenty48Cell]:
180        for row, row_cells in enumerate(self.board):
181            for col, cell_value in enumerate(row_cells):
182                if col == col_index:
183                    yield (row, col, cell_value)
184
185    def non_zero_row_values(self, index: int) -> Tuple[List, List]:
186        non_zero_values = [
187            value for row, col, value in self.row(index) if value != 0
188        ]
189        padding = [0] * (self.width - len(non_zero_values))
190        return (non_zero_values, padding)
191
192    def move_right(self) -> None:
193        for i in range(self.height):
194            non_zero_values, padding = self.non_zero_row_values(i)
195            self.board[i] = padding + non_zero_values
196
197    def move_left(self) -> None:
198        for i in range(self.height):
199            non_zero_values, padding = self.non_zero_row_values(i)
200            self.board[i] = non_zero_values + padding
201
202    def add_horizontal(self, reverse=False) -> None:
203        for i in range(self.width):
204            this_row = list(self.row(i))
205            if reverse:
206                this_row = list(reversed(this_row))
207            for row, col, this_cell in this_row:
208                if this_cell == 0 or col >= self.width - 1:
209                    continue
210                next_cell = self.board[row][col + 1]
211                if this_cell == next_cell:
212                    self.board[row][col] = 0
213                    self.board[row][col + 1] = this_cell * 2
214                    break
215
216    def non_zero_col_values(self, index: int) -> Tuple[List, List]:
217        non_zero_values = [
218            value for row, col, value in self.col(index) if value != 0
219        ]
220        padding = [0] * (self.height - len(non_zero_values))
221        return (non_zero_values, padding)
222
223    def _set_column(self, col_index: int, values: List[int]) -> None:
224        for row, value in enumerate(values):
225            self.board[row][col_index] = value
226
227    def add_vertical(self, reverse=False) -> None:
228        for i in range(self.height):
229            this_column = list(self.col(i))
230            if reverse:
231                this_column = list(reversed(this_column))
232            for row, col, this_cell in this_column:
233                if this_cell == 0 or row >= self.height - 1:
234                    continue
235                next_cell = self.board[row + 1][col]
236                if this_cell == next_cell:
237                    self.board[row][col] = 0
238                    self.board[row + 1][col] = this_cell * 2
239                    break
240
241    def move_down(self) -> None:
242        for col_index in range(self.width):
243            non_zero_values, padding = self.non_zero_col_values(col_index)
244            self._set_column(col_index, padding + non_zero_values)
245
246    def move_up(self) -> None:
247        for col_index in range(self.width):
248            non_zero_values, padding = self.non_zero_col_values(col_index)
249            self._set_column(col_index, non_zero_values + padding)
250
251    def press_down(self) -> None:
252        self.move_down()
253        self.add_vertical(reverse=True)
254        self.move_down()
255        self.complete_move()
256
257    def press_up(self) -> None:
258        self.move_up()
259        self.add_vertical()
260        self.move_up()
261        self.complete_move()
262
263    def press_right(self) -> None:
264        self.move_right()
265        self.add_horizontal(reverse=True)
266        self.move_right()
267        self.complete_move()
268
269    def press_left(self) -> None:
270        self.move_left()
271        self.add_horizontal()
272        self.move_left()
273        self.complete_move()
274
275
276class Twenty48Control(FormattedTextControl):
277    """Example prompt_toolkit UIControl for displaying formatted text.
278
279    This is the prompt_toolkit class that is responsible for drawing the 2048,
280    handling keybindings if in focus, and mouse input.
281    """
282    def __init__(self, twenty48_pane: 'Twenty48Pane', *args, **kwargs) -> None:
283        self.twenty48_pane = twenty48_pane
284        self.game = self.twenty48_pane.game
285
286        # Set some custom key bindings to toggle the view mode and wrap lines.
287        key_bindings = KeyBindings()
288
289        @key_bindings.add('R')
290        def _restart(_event: KeyPressEvent) -> None:
291            """Restart the game."""
292            self.game.reset_game()
293
294        @key_bindings.add('q')
295        def _quit(_event: KeyPressEvent) -> None:
296            """Quit the game."""
297            self.twenty48_pane.close_dialog()
298
299        @key_bindings.add('j')
300        @key_bindings.add('down')
301        def _move_down(_event: KeyPressEvent) -> None:
302            """Move down"""
303            self.game.press_down()
304
305        @key_bindings.add('k')
306        @key_bindings.add('up')
307        def _move_up(_event: KeyPressEvent) -> None:
308            """Move up."""
309            self.game.press_up()
310
311        @key_bindings.add('h')
312        @key_bindings.add('left')
313        def _move_left(_event: KeyPressEvent) -> None:
314            """Move left."""
315            self.game.press_left()
316
317        @key_bindings.add('l')
318        @key_bindings.add('right')
319        def _move_right(_event: KeyPressEvent) -> None:
320            """Move right."""
321            self.game.press_right()
322
323        # Include the key_bindings keyword arg when passing to the parent class
324        # __init__ function.
325        kwargs['key_bindings'] = key_bindings
326        # Call the parent FormattedTextControl.__init__
327        super().__init__(*args, **kwargs)
328
329    def mouse_handler(self, mouse_event: MouseEvent):
330        """Mouse handler for this control."""
331        # If the user clicks anywhere this function is run.
332
333        # Mouse positions relative to this control. x is the column starting
334        # from the left size as zero. y is the row starting with the top as
335        # zero.
336        _click_x = mouse_event.position.x
337        _click_y = mouse_event.position.y
338
339        # Mouse click behavior usually depends on if this window pane is in
340        # focus. If not in focus, then focus on it when left clicking. If
341        # already in focus then perform the action specific to this window.
342
343        # If not in focus, change focus to this 2048 pane and do nothing else.
344        if not has_focus(self.twenty48_pane)():
345            if mouse_event.event_type == MouseEventType.MOUSE_UP:
346                get_pw_console_app().focus_on_container(self.twenty48_pane)
347                # Mouse event handled, return None.
348                return None
349
350        # If code reaches this point, this window is already in focus.
351        # if mouse_event.event_type == MouseEventType.MOUSE_UP:
352        #     # Toggle the view mode.
353        #     self.twenty48_pane.toggle_view_mode()
354        #     # Mouse event handled, return None.
355        #     return None
356
357        # Mouse event not handled, return NotImplemented.
358        return NotImplemented
359
360
361class Twenty48Pane(FloatingWindowPane, PluginMixin):
362    """Example Pigweed Console plugin to play 2048.
363
364    The Twenty48Pane is a WindowPane based plugin that displays an interactive
365    game of 2048. It inherits from both WindowPane and PluginMixin. It can be
366    added on console startup by calling: ::
367
368        my_console.add_window_plugin(Twenty48Pane())
369
370    For an example see:
371    https://pigweed.dev/pw_console/embedding.html#adding-plugins
372    """
373    def __init__(self, include_resize_handle: bool = True, **kwargs):
374
375        super().__init__(pane_title='2048',
376                         height=Dimension(preferred=17),
377                         width=Dimension(preferred=50),
378                         **kwargs)
379        self.game = Twenty48Game()
380
381        # Hide by default.
382        self.show_pane = False
383
384        # Create a toolbar for display at the bottom of the 2048 window. It
385        # will show the window title and buttons.
386        self.bottom_toolbar = WindowPaneToolbar(
387            self, include_resize_handle=include_resize_handle)
388
389        # Add a button to restart the game.
390        self.bottom_toolbar.add_button(
391            ToolbarButton(
392                key='R',  # Key binding help text for this function
393                description='Restart',  # Button name
394                # Function to run when clicked.
395                mouse_handler=self.game.reset_game,
396            ))
397        # Add a button to restart the game.
398        self.bottom_toolbar.add_button(
399            ToolbarButton(
400                key='q',  # Key binding help text for this function
401                description='Quit',  # Button name
402                # Function to run when clicked.
403                mouse_handler=self.close_dialog,
404            ))
405
406        # Every FormattedTextControl object (Twenty48Control) needs to live
407        # inside a prompt_toolkit Window() instance. Here is where you specify
408        # alignment, style, and dimensions. See the prompt_toolkit docs for all
409        # opitons:
410        # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.layout.Window
411        self.twenty48_game_window = Window(
412            # Set the content to a Twenty48Control instance.
413            content=Twenty48Control(
414                self,  # This Twenty48Pane class
415                self.game,  # Content from Twenty48Game.__pt_formatted_text__()
416                show_cursor=False,
417                focusable=True,
418            ),
419            # Make content left aligned
420            align=WindowAlign.LEFT,
421            # These two set to false make this window fill all available space.
422            dont_extend_width=True,
423            dont_extend_height=False,
424            wrap_lines=False,
425            width=Dimension(preferred=28),
426            height=Dimension(preferred=15),
427        )
428
429        self.twenty48_stats_window = Window(
430            content=Twenty48Control(
431                self,  # This Twenty48Pane class
432                self.game.stats,  # Content from Twenty48Game.stats()
433                show_cursor=False,
434                focusable=True,
435            ),
436            # Make content left aligned
437            align=WindowAlign.LEFT,
438            # These two set to false make this window fill all available space.
439            width=Dimension(preferred=20),
440            dont_extend_width=False,
441            dont_extend_height=False,
442            wrap_lines=False,
443        )
444
445        # self.container is the root container that contains objects to be
446        # rendered in the UI, one on top of the other.
447        self.container = self._create_pane_container(
448            pw_console.widgets.border.create_border(
449                HSplit([
450                    # Vertical split content
451                    VSplit([
452                        # Left side will show the game board.
453                        self.twenty48_game_window,
454                        # Stats will be shown on the right.
455                        self.twenty48_stats_window,
456                    ]),
457                    # The bottom_toolbar is shown below the VSplit.
458                    self.bottom_toolbar,
459                ]),
460                title='2048',
461                border_style='class:command-runner-border',
462                # left_margin_columns=1,
463                # right_margin_columns=1,
464            ))
465
466        self.dialog_content: List[AnyContainer] = [
467            # Vertical split content
468            VSplit([
469                # Left side will show the game board.
470                self.twenty48_game_window,
471                # Stats will be shown on the right.
472                self.twenty48_stats_window,
473            ]),
474            # The bottom_toolbar is shown below the VSplit.
475            self.bottom_toolbar,
476        ]
477        # Wrap the dialog content in a border
478        self.bordered_dialog_content = pw_console.widgets.border.create_border(
479            HSplit(self.dialog_content),
480            title='2048',
481            border_style='class:command-runner-border',
482        )
483        # self.container is the root container that contains objects to be
484        # rendered in the UI, one on top of the other.
485        if include_resize_handle:
486            self.container = self._create_pane_container(*self.dialog_content)
487        else:
488            self.container = self._create_pane_container(
489                self.bordered_dialog_content)
490
491        # This plugin needs to run a task in the background periodically and
492        # uses self.plugin_init() to set which function to run, and how often.
493        # This is provided by PluginMixin. See the docs for more info:
494        # https://pigweed.dev/pw_console/plugins.html#background-tasks
495        self.plugin_init(
496            plugin_callback=self._background_task,
497            # Run self._background_task once per second.
498            plugin_callback_frequency=1.0,
499            plugin_logger_name='pw_console_example_2048_plugin',
500        )
501
502    def get_top_level_menus(self) -> List[MenuItem]:
503        def _toggle_dialog() -> None:
504            self.toggle_dialog()
505
506        return [
507            MenuItem(
508                '[2048]',
509                children=[
510                    MenuItem('Example Top Level Menu',
511                             handler=None,
512                             disabled=True),
513                    # Menu separator
514                    MenuItem('-', None),
515                    MenuItem('Show/Hide 2048 Game', handler=_toggle_dialog),
516                    MenuItem('Restart', handler=self.game.reset_game),
517                ],
518            ),
519        ]
520
521    def pw_console_init(self, app: 'ConsoleApp') -> None:
522        """Set the Pigweed Console application instance.
523
524        This function is called after the Pigweed Console starts up and allows
525        access to the user preferences. Prefs is required for creating new
526        user-remappable keybinds."""
527        self.application = app
528
529    def _background_task(self) -> bool:
530        """Function run in the background for the ClockPane plugin."""
531        # Optional: make a log message for debugging purposes. For more info
532        # see:
533        # https://pigweed.dev/pw_console/plugins.html#debugging-plugin-behavior
534        # self.plugin_logger.debug('background_task_update_count: %s',
535        #                          self.background_task_update_count)
536
537        # Returning True in the background task will force the user interface to
538        # re-draw.
539        # Returning False means no updates required.
540
541        if self.show_pane:
542            # Return true so the game clock is updated.
543            return True
544
545        # Game window is hidden, don't redraw.
546        return False