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:
Create a new Python class inheriting from either WindowPane or WindowPaneToolbar.
Optionally inherit from The
PluginMixin
class as well for running background tasks.
Enable the plugin before pw_console startup by calling
add_window_plugin
,add_floating_window_plugin
,add_top_toolbar
oradd_bottom_toolbar
. See the Adding Plugins section of the Embedding Guide for an example.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_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#
Pigweed AI summary: The article discusses how to debug plugin behavior when using background threads for updating. It can be difficult to see errors and exceptions may not be visible. The solution is to use PluginMixin and specify a name for a Python logger to use with the plugin_logger_name keyword argument. This allows the user to open up a new log window while the console is running to see what the plugin is doing. The logger name can be accessed by clicking in the main menu: File > Open Logger > my_awesome_plugin
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 AI summary: The Pigweed Console offers sample plugins that can be used as templates for creating custom plugins. These include the Bandwidth Toolbar, Calculator, Clock, and 2048 Game plugins. The Bandwidth Toolbar tracks and logs data sent and received over a serial transport, while the Calculator plugin is a full window that can be moved around the user interface and allows users to input math equations. The Clock plugin displays a clock and some formatted text examples, and the 2048 Game plugin demonstrates more complex user interaction
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.
Bandwidth Toolbar#
Pigweed AI summary: The Bandwidth Toolbar is a tool that tracks and logs data sent and received over a serial transport, such as a socket or PySerial device. It can be used in a custom transport interface by instantiating the SerialBandwidthTracker and calling track_read_data for incoming data bytes and track_write_data for outgoing data bytes.
Tracks and logs the data sent and received over a serial transport like a socket
or PySerial device. To use in a custom transport interface instantiate the
SerialBandwidthTracker
and call track_read_data
on incoming data bytes
and track_write_data
on outoing data bytes.
Calculator#
Pigweed AI summary: The Calculator plugin in Pigweed Console is a full window that can be moved around the user interface and allows users to type in math equations in an input prompt displayed at the bottom of the window. When the enter key is pressed, the input is processed and the result is shown in the top half of the window. Both input and output fields are prompt_toolkit TextArea objects that can have their own options like syntax highlighting. The code is heavily commented and describes what each line is doing.
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.#
The code is heavily commented and describes what each line is doing. See the Code Listing: calc_pane.py for the full source.
Clock#
Pigweed AI summary: The ClockPane is a WindowPane based plugin that displays a clock and formatted text examples. It uses PluginMixin to run a task in the background and has toolbar buttons to toggle view mode and line wrapping. The code is heavily commented to guide plugin authors through development.
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.#
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.#
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#
Pigweed AI summary: The 2048 Game plugin is a demonstration of complex user interaction in Pigweed Console. It uses the Twenty48Pane class to create a floating window and adds custom game keybindings. The Twenty48Game class manages the game state and prints the game board using prompt_toolkit style and text tuples. A screenshot of the game board is shown in the figure.
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_toolkitFormattedTextControl
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.#
Appendix#
Code Listing: calc_pane.py
#
Pigweed AI summary: This is a code listing for a Python plugin called CalcPane, which is an example text input-output plugin. It accepts text input and displays output, similar to a full-screen calculator. The plugin is 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 is shown in the top
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
46 def __init__(self):
47 # Call WindowPane.__init__ and set the title to 'Calculator'
48 super().__init__(pane_title='Calculator')
49
50 # Create a TextArea for the output-field
51 # TextArea is a prompt_toolkit widget that can display editable text in
52 # a buffer. See the prompt_toolkit docs for all possible options:
53 # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.widgets.TextArea
54 self.output_field = TextArea(
55 # Optional Styles to apply to this TextArea
56 style='class:output-field',
57 # Initial text to put into the buffer.
58 text='Calculator Output',
59 # Allow this buffer to be in focus. This lets you drag select text
60 # contained inside, and edit the contents unless readonly.
61 focusable=True,
62 # Focus on mouse click.
63 focus_on_click=True,
64 )
65
66 # This is the search toolbar and only appears if the user presses ctrl-r
67 # to do reverse history search (similar to bash or zsh). Its used by the
68 # input_field below.
69 self.search_field = SearchToolbar()
70
71 # Create a TextArea for the user input.
72 self.input_field = TextArea(
73 # The height is set to 1 line
74 height=1,
75 # Prompt string that appears before the cursor.
76 prompt='>>> ',
77 # Optional Styles to apply to this TextArea
78 style='class:input-field',
79 # We only allow one line input for this example but multiline is
80 # supported by prompt_toolkit.
81 multiline=False,
82 wrap_lines=False,
83 # Allow reverse history search
84 search_field=self.search_field,
85 # Allow this input to be focused.
86 focusable=True,
87 # Focus on mouse click.
88 focus_on_click=True,
89 )
90
91 # The TextArea accept_handler function is called by prompt_toolkit (the
92 # UI) when the user presses enter. Here we override it to our own accept
93 # handler defined in this CalcPane class.
94 self.input_field.accept_handler = self.accept_input
95
96 # Create a toolbar for display at the bottom of this window. It will
97 # show the window title and toolbar buttons.
98 self.bottom_toolbar = WindowPaneToolbar(self)
99 self.bottom_toolbar.add_button(
100 ToolbarButton(
101 key='Enter', # Key binding for this function
102 description='Run Calculation', # Button name
103 # Function to run when clicked.
104 mouse_handler=self.run_calculation,
105 )
106 )
107 self.bottom_toolbar.add_button(
108 ToolbarButton(
109 key='Ctrl-c', # Key binding for this function
110 description='Copy Output', # Button name
111 # Function to run when clicked.
112 mouse_handler=self.copy_all_output,
113 )
114 )
115
116 # self.container is the root container that contains objects to be
117 # rendered in the UI, one on top of the other.
118 self.container = self._create_pane_container(
119 # Show the output_field on top
120 self.output_field,
121 # Draw a separator line with height=1
122 Window(height=1, char='─', style='class:line'),
123 # Show the input field just below that.
124 self.input_field,
125 # If ctrl-r reverse history is active, show the search box below the
126 # input_field.
127 self.search_field,
128 # Lastly, show the toolbar.
129 self.bottom_toolbar,
130 )
131
132 def pw_console_init(self, app: 'ConsoleApp') -> None:
133 """Set the Pigweed Console application instance.
134
135 This function is called after the Pigweed Console starts up and allows
136 access to the user preferences. Prefs is required for creating new
137 user-remappable keybinds."""
138 self.application = app
139 self.set_custom_keybinds()
140
141 def set_custom_keybinds(self) -> None:
142 # Fetch ConsoleApp preferences to load user keybindings
143 prefs = self.application.prefs
144 # Register a named keybind function that is user re-mappable
145 prefs.register_named_key_function(
146 'calc-pane.copy-selected-text',
147 # default bindings
148 ['c-c'],
149 )
150
151 # For setting additional keybindings to the output_field.
152 key_bindings = KeyBindings()
153
154 # Map the 'calc-pane.copy-selected-text' function keybind to the
155 # _copy_all_output function below. This will set
156 @prefs.register_keybinding('calc-pane.copy-selected-text', key_bindings)
157 def _copy_all_output(_event: KeyPressEvent) -> None:
158 """Copy selected text from the output buffer."""
159 self.copy_selected_output()
160
161 # Set the output_field controls key_bindings to the new bindings.
162 self.output_field.control.key_bindings = key_bindings
163
164 def run_calculation(self):
165 """Trigger the input_field's accept_handler.
166
167 This has the same effect as pressing enter in the input_field.
168 """
169 self.input_field.buffer.validate_and_handle()
170
171 def accept_input(self, _buffer):
172 """Function run when the user presses enter in the input_field.
173
174 Takes a buffer argument that contains the user's input text.
175 """
176 # Evaluate the user's calculator expression as Python and format the
177 # output result.
178 try:
179 output = "\n\nIn: {}\nOut: {}".format(
180 self.input_field.text,
181 # NOTE: Don't use 'eval' in real code (this is just an example)
182 eval(self.input_field.text), # pylint: disable=eval-used
183 )
184 except BaseException as exception: # pylint: disable=broad-except
185 output = "\n\n{}".format(exception)
186
187 # Append the new output result to the existing output_field contents.
188 new_text = self.output_field.text + output
189
190 # Update the output_field with the new contents and move the
191 # cursor_position to the end.
192 self.output_field.buffer.document = Document(
193 text=new_text, cursor_position=len(new_text)
194 )
195
196 def copy_selected_output(self):
197 """Copy highlighted text in the output_field to the system clipboard."""
198 clipboard_data = self.output_field.buffer.copy_selection()
199 self.application.application.clipboard.set_data(clipboard_data)
200
201 def copy_all_output(self):
202 """Copy all text in the output_field to the system clipboard."""
203 self.application.application.clipboard.set_text(
204 self.output_field.buffer.text
205 )
Code Listing: clock_pane.py
#
Pigweed AI summary: The given paragraph is a code listing of a Python file named "clock_pane.py". It is a Pigweed Console plugin that displays a clock and examples of text formatting. The code includes classes for the clock control, clock pane, and helper functions. The clock control class is responsible for drawing the clock, handling key bindings, and mouse input. The clock pane class is a window plugin that displays the clock and formatted text examples. It includes functions to toggle the view mode and line wrapping, as
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)
Code Listing: twenty48_pane.py
#
Pigweed AI summary: The given paragraph is a code listing of a Python script that implements the game 2048. The script includes classes and functions for managing the game board, handling user input, and displaying the game interface. The code uses the prompt_toolkit library for creating a graphical user interface and handling keyboard and mouse events. The game logic is implemented in the Twenty48Game class, which includes methods for moving tiles, merging them, and updating the game state. The Twenty48Pane class is a plugin for the
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
35from pw_console.widgets import (
36 create_border,
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
53 def __init__(self) -> None:
54 self.colors = {
55 2: 'bg:#dd6',
56 4: 'bg:#da6',
57 8: 'bg:#d86',
58 16: 'bg:#d66',
59 32: 'bg:#d6a',
60 64: 'bg:#a6d',
61 128: 'bg:#66d',
62 256: 'bg:#68a',
63 512: 'bg:#6a8',
64 1024: 'bg:#6d6',
65 2048: 'bg:#0f8',
66 4096: 'bg:#0ff',
67 }
68 self.board: List[List[int]]
69 self.last_board: List[Twenty48Cell]
70 self.move_count: int
71 self.width: int = 4
72 self.height: int = 4
73 self.max_value: int = 0
74 self.start_time: float
75 self.reset_game()
76
77 def reset_game(self) -> None:
78 self.start_time = time.time()
79 self.max_value = 2
80 self.move_count = 0
81 self.board = []
82 for _i in range(self.height):
83 self.board.append([0] * self.width)
84 self.last_board = list(self.all_cells())
85 self.add_random_tiles(2)
86
87 def stats(self) -> StyleAndTextTuples:
88 """Returns stats on the game in progress."""
89 elapsed_time = int(time.time() - self.start_time)
90 minutes = int(elapsed_time / 60.0)
91 seconds = elapsed_time % 60
92 fragments: StyleAndTextTuples = []
93 fragments.append(('', '\n'))
94 fragments.append(('', f'Moves: {self.move_count}'))
95 fragments.append(('', '\n'))
96 fragments.append(('', 'Time: {:0>2}:{:0>2}'.format(minutes, seconds)))
97 fragments.append(('', '\n'))
98 fragments.append(('', f'Max: {self.max_value}'))
99 fragments.append(('', '\n\n'))
100 fragments.append(('', 'Press R to restart\n'))
101 fragments.append(('', '\n'))
102 fragments.append(('', 'Arrow keys to move'))
103 return fragments
104
105 def __pt_formatted_text__(self) -> StyleAndTextTuples:
106 """Returns the game board formatted in a grid with colors."""
107 fragments: StyleAndTextTuples = []
108
109 def print_row(row: List[int], include_number: bool = False) -> None:
110 fragments.append(('', ' '))
111 for col in row:
112 style = 'class:theme-fg-default '
113 if col > 0:
114 style = '#000 '
115 style += self.colors.get(col, '')
116 text = ' ' * 6
117 if include_number:
118 text = '{:^6}'.format(col)
119 fragments.append((style, text))
120 fragments.append(('', '\n'))
121
122 fragments.append(('', '\n'))
123 for row in self.board:
124 print_row(row)
125 print_row(row, include_number=True)
126 print_row(row)
127
128 return fragments
129
130 def __repr__(self) -> str:
131 board = ''
132 for row_cells in self.board:
133 for column in row_cells:
134 board += '{:^6}'.format(column)
135 board += '\n'
136 return board
137
138 def all_cells(self) -> Iterable[Twenty48Cell]:
139 for row, row_cells in enumerate(self.board):
140 for col, cell_value in enumerate(row_cells):
141 yield (row, col, cell_value)
142
143 def update_max_value(self) -> None:
144 for _row, _col, value in self.all_cells():
145 if value > self.max_value:
146 self.max_value = value
147
148 def empty_cells(self) -> Iterable[Twenty48Cell]:
149 for row, row_cells in enumerate(self.board):
150 for col, cell_value in enumerate(row_cells):
151 if cell_value != 0:
152 continue
153 yield (row, col, cell_value)
154
155 def _board_changed(self) -> bool:
156 return self.last_board != list(self.all_cells())
157
158 def complete_move(self) -> None:
159 if not self._board_changed():
160 # Move did nothing, ignore.
161 return
162
163 self.update_max_value()
164 self.move_count += 1
165 self.add_random_tiles()
166 self.last_board = list(self.all_cells())
167
168 def add_random_tiles(self, count: int = 1) -> None:
169 for _i in range(count):
170 empty_cells = list(self.empty_cells())
171 if not empty_cells:
172 return
173 row, col, _value = choice(empty_cells)
174 self.board[row][col] = 2
175
176 def row(self, row_index: int) -> Iterable[Twenty48Cell]:
177 for col, cell_value in enumerate(self.board[row_index]):
178 yield (row_index, col, cell_value)
179
180 def col(self, col_index: int) -> Iterable[Twenty48Cell]:
181 for row, row_cells in enumerate(self.board):
182 for col, cell_value in enumerate(row_cells):
183 if col == col_index:
184 yield (row, col, cell_value)
185
186 def non_zero_row_values(self, index: int) -> Tuple[List, List]:
187 non_zero_values = [
188 value for row, col, value in self.row(index) if value != 0
189 ]
190 padding = [0] * (self.width - len(non_zero_values))
191 return (non_zero_values, padding)
192
193 def move_right(self) -> None:
194 for i in range(self.height):
195 non_zero_values, padding = self.non_zero_row_values(i)
196 self.board[i] = padding + non_zero_values
197
198 def move_left(self) -> None:
199 for i in range(self.height):
200 non_zero_values, padding = self.non_zero_row_values(i)
201 self.board[i] = non_zero_values + padding
202
203 def add_horizontal(self, reverse=False) -> None:
204 for i in range(self.width):
205 this_row = list(self.row(i))
206 if reverse:
207 this_row = list(reversed(this_row))
208 for row, col, this_cell in this_row:
209 if this_cell == 0 or col >= self.width - 1:
210 continue
211 next_cell = self.board[row][col + 1]
212 if this_cell == next_cell:
213 self.board[row][col] = 0
214 self.board[row][col + 1] = this_cell * 2
215 break
216
217 def non_zero_col_values(self, index: int) -> Tuple[List, List]:
218 non_zero_values = [
219 value for row, col, value in self.col(index) if value != 0
220 ]
221 padding = [0] * (self.height - len(non_zero_values))
222 return (non_zero_values, padding)
223
224 def _set_column(self, col_index: int, values: List[int]) -> None:
225 for row, value in enumerate(values):
226 self.board[row][col_index] = value
227
228 def add_vertical(self, reverse=False) -> None:
229 for i in range(self.height):
230 this_column = list(self.col(i))
231 if reverse:
232 this_column = list(reversed(this_column))
233 for row, col, this_cell in this_column:
234 if this_cell == 0 or row >= self.height - 1:
235 continue
236 next_cell = self.board[row + 1][col]
237 if this_cell == next_cell:
238 self.board[row][col] = 0
239 self.board[row + 1][col] = this_cell * 2
240 break
241
242 def move_down(self) -> None:
243 for col_index in range(self.width):
244 non_zero_values, padding = self.non_zero_col_values(col_index)
245 self._set_column(col_index, padding + non_zero_values)
246
247 def move_up(self) -> None:
248 for col_index in range(self.width):
249 non_zero_values, padding = self.non_zero_col_values(col_index)
250 self._set_column(col_index, non_zero_values + padding)
251
252 def press_down(self) -> None:
253 self.move_down()
254 self.add_vertical(reverse=True)
255 self.move_down()
256 self.complete_move()
257
258 def press_up(self) -> None:
259 self.move_up()
260 self.add_vertical()
261 self.move_up()
262 self.complete_move()
263
264 def press_right(self) -> None:
265 self.move_right()
266 self.add_horizontal(reverse=True)
267 self.move_right()
268 self.complete_move()
269
270 def press_left(self) -> None:
271 self.move_left()
272 self.add_horizontal()
273 self.move_left()
274 self.complete_move()
275
276
277class Twenty48Control(FormattedTextControl):
278 """Example prompt_toolkit UIControl for displaying formatted text.
279
280 This is the prompt_toolkit class that is responsible for drawing the 2048,
281 handling keybindings if in focus, and mouse input.
282 """
283
284 def __init__(self, twenty48_pane: 'Twenty48Pane', *args, **kwargs) -> None:
285 self.twenty48_pane = twenty48_pane
286 self.game = self.twenty48_pane.game
287
288 # Set some custom key bindings to toggle the view mode and wrap lines.
289 key_bindings = KeyBindings()
290
291 @key_bindings.add('R')
292 def _restart(_event: KeyPressEvent) -> None:
293 """Restart the game."""
294 self.game.reset_game()
295
296 @key_bindings.add('q')
297 def _quit(_event: KeyPressEvent) -> None:
298 """Quit the game."""
299 self.twenty48_pane.close_dialog()
300
301 @key_bindings.add('j')
302 @key_bindings.add('down')
303 def _move_down(_event: KeyPressEvent) -> None:
304 """Move down"""
305 self.game.press_down()
306
307 @key_bindings.add('k')
308 @key_bindings.add('up')
309 def _move_up(_event: KeyPressEvent) -> None:
310 """Move up."""
311 self.game.press_up()
312
313 @key_bindings.add('h')
314 @key_bindings.add('left')
315 def _move_left(_event: KeyPressEvent) -> None:
316 """Move left."""
317 self.game.press_left()
318
319 @key_bindings.add('l')
320 @key_bindings.add('right')
321 def _move_right(_event: KeyPressEvent) -> None:
322 """Move right."""
323 self.game.press_right()
324
325 # Include the key_bindings keyword arg when passing to the parent class
326 # __init__ function.
327 kwargs['key_bindings'] = key_bindings
328 # Call the parent FormattedTextControl.__init__
329 super().__init__(*args, **kwargs)
330
331 def mouse_handler(self, mouse_event: MouseEvent):
332 """Mouse handler for this control."""
333 # If the user clicks anywhere this function is run.
334
335 # Mouse positions relative to this control. x is the column starting
336 # from the left size as zero. y is the row starting with the top as
337 # zero.
338 _click_x = mouse_event.position.x
339 _click_y = mouse_event.position.y
340
341 # Mouse click behavior usually depends on if this window pane is in
342 # focus. If not in focus, then focus on it when left clicking. If
343 # already in focus then perform the action specific to this window.
344
345 # If not in focus, change focus to this 2048 pane and do nothing else.
346 if not has_focus(self.twenty48_pane)():
347 if mouse_event.event_type == MouseEventType.MOUSE_UP:
348 get_pw_console_app().focus_on_container(self.twenty48_pane)
349 # Mouse event handled, return None.
350 return None
351
352 # If code reaches this point, this window is already in focus.
353 # if mouse_event.event_type == MouseEventType.MOUSE_UP:
354 # # Toggle the view mode.
355 # self.twenty48_pane.toggle_view_mode()
356 # # Mouse event handled, return None.
357 # return None
358
359 # Mouse event not handled, return NotImplemented.
360 return NotImplemented
361
362
363class Twenty48Pane(FloatingWindowPane, PluginMixin):
364 """Example Pigweed Console plugin to play 2048.
365
366 The Twenty48Pane is a WindowPane based plugin that displays an interactive
367 game of 2048. It inherits from both WindowPane and PluginMixin. It can be
368 added on console startup by calling: ::
369
370 my_console.add_window_plugin(Twenty48Pane())
371
372 For an example see:
373 https://pigweed.dev/pw_console/embedding.html#adding-plugins
374 """
375
376 def __init__(self, include_resize_handle: bool = True, **kwargs):
377 super().__init__(
378 pane_title='2048',
379 height=Dimension(preferred=17),
380 width=Dimension(preferred=50),
381 **kwargs,
382 )
383 self.game = Twenty48Game()
384
385 # Hide by default.
386 self.show_pane = False
387
388 # Create a toolbar for display at the bottom of the 2048 window. It
389 # will show the window title and buttons.
390 self.bottom_toolbar = WindowPaneToolbar(
391 self, include_resize_handle=include_resize_handle
392 )
393
394 # Add a button to restart the game.
395 self.bottom_toolbar.add_button(
396 ToolbarButton(
397 key='R', # Key binding help text for this function
398 description='Restart', # Button name
399 # Function to run when clicked.
400 mouse_handler=self.game.reset_game,
401 )
402 )
403 # Add a button to restart the game.
404 self.bottom_toolbar.add_button(
405 ToolbarButton(
406 key='q', # Key binding help text for this function
407 description='Quit', # Button name
408 # Function to run when clicked.
409 mouse_handler=self.close_dialog,
410 )
411 )
412
413 # Every FormattedTextControl object (Twenty48Control) needs to live
414 # inside a prompt_toolkit Window() instance. Here is where you specify
415 # alignment, style, and dimensions. See the prompt_toolkit docs for all
416 # opitons:
417 # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.layout.Window
418 self.twenty48_game_window = Window(
419 # Set the content to a Twenty48Control instance.
420 content=Twenty48Control(
421 self, # This Twenty48Pane class
422 self.game, # Content from Twenty48Game.__pt_formatted_text__()
423 show_cursor=False,
424 focusable=True,
425 ),
426 # Make content left aligned
427 align=WindowAlign.LEFT,
428 # These two set to false make this window fill all available space.
429 dont_extend_width=True,
430 dont_extend_height=False,
431 wrap_lines=False,
432 width=Dimension(preferred=28),
433 height=Dimension(preferred=15),
434 )
435
436 self.twenty48_stats_window = Window(
437 content=Twenty48Control(
438 self, # This Twenty48Pane class
439 self.game.stats, # Content from Twenty48Game.stats()
440 show_cursor=False,
441 focusable=True,
442 ),
443 # Make content left aligned
444 align=WindowAlign.LEFT,
445 # These two set to false make this window fill all available space.
446 width=Dimension(preferred=20),
447 dont_extend_width=False,
448 dont_extend_height=False,
449 wrap_lines=False,
450 )
451
452 # self.container is the root container that contains objects to be
453 # rendered in the UI, one on top of the other.
454 self.container = self._create_pane_container(
455 create_border(
456 HSplit(
457 [
458 # Vertical split content
459 VSplit(
460 [
461 # Left side will show the game board.
462 self.twenty48_game_window,
463 # Stats will be shown on the right.
464 self.twenty48_stats_window,
465 ]
466 ),
467 # The bottom_toolbar is shown below the VSplit.
468 self.bottom_toolbar,
469 ]
470 ),
471 title='2048',
472 border_style='class:command-runner-border',
473 # left_margin_columns=1,
474 # right_margin_columns=1,
475 )
476 )
477
478 self.dialog_content: List[AnyContainer] = [
479 # Vertical split content
480 VSplit(
481 [
482 # Left side will show the game board.
483 self.twenty48_game_window,
484 # Stats will be shown on the right.
485 self.twenty48_stats_window,
486 ]
487 ),
488 # The bottom_toolbar is shown below the VSplit.
489 self.bottom_toolbar,
490 ]
491 # Wrap the dialog content in a border
492 self.bordered_dialog_content = create_border(
493 HSplit(self.dialog_content),
494 title='2048',
495 border_style='class:command-runner-border',
496 )
497 # self.container is the root container that contains objects to be
498 # rendered in the UI, one on top of the other.
499 if include_resize_handle:
500 self.container = self._create_pane_container(*self.dialog_content)
501 else:
502 self.container = self._create_pane_container(
503 self.bordered_dialog_content
504 )
505
506 # This plugin needs to run a task in the background periodically and
507 # uses self.plugin_init() to set which function to run, and how often.
508 # This is provided by PluginMixin. See the docs for more info:
509 # https://pigweed.dev/pw_console/plugins.html#background-tasks
510 self.plugin_init(
511 plugin_callback=self._background_task,
512 # Run self._background_task once per second.
513 plugin_callback_frequency=1.0,
514 plugin_logger_name='pw_console_example_2048_plugin',
515 )
516
517 def get_top_level_menus(self) -> List[MenuItem]:
518 def _toggle_dialog() -> None:
519 self.toggle_dialog()
520
521 return [
522 MenuItem(
523 '[2048]',
524 children=[
525 MenuItem(
526 'Example Top Level Menu', handler=None, disabled=True
527 ),
528 # Menu separator
529 MenuItem('-', None),
530 MenuItem('Show/Hide 2048 Game', handler=_toggle_dialog),
531 MenuItem('Restart', handler=self.game.reset_game),
532 ],
533 ),
534 ]
535
536 def pw_console_init(self, app: 'ConsoleApp') -> None:
537 """Set the Pigweed Console application instance.
538
539 This function is called after the Pigweed Console starts up and allows
540 access to the user preferences. Prefs is required for creating new
541 user-remappable keybinds."""
542 self.application = app
543
544 def _background_task(self) -> bool:
545 """Function run in the background for the ClockPane plugin."""
546 # Optional: make a log message for debugging purposes. For more info
547 # see:
548 # https://pigweed.dev/pw_console/plugins.html#debugging-plugin-behavior
549 # self.plugin_logger.debug('background_task_update_count: %s',
550 # self.background_task_update_count)
551
552 # Returning True in the background task will force the user interface to
553 # re-draw.
554 # Returning False means no updates required.
555
556 if self.show_pane:
557 # Return true so the game clock is updated.
558 return True
559
560 # Game window is hidden, don't redraw.
561 return False