blob: 45957db3a90ccdee95887a4ab80f110ff99f36aa [file] [log] [blame]
# Copyright 2021 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Example Plugin that displays some dynamic content (a clock) and examples of
text formatting."""
from datetime import datetime
from prompt_toolkit.filters import Condition, has_focus
from prompt_toolkit.formatted_text import (
FormattedText,
HTML,
merge_formatted_text,
)
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
from prompt_toolkit.layout import FormattedTextControl, Window, WindowAlign
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
from pw_console.plugin_mixin import PluginMixin
from pw_console.widgets import ToolbarButton, WindowPane, WindowPaneToolbar
from pw_console.get_pw_console_app import get_pw_console_app
# Helper class used by the ClockPane plugin for displaying dynamic text,
# handling key bindings and mouse input. See the ClockPane class below for the
# beginning of the plugin implementation.
class ClockControl(FormattedTextControl):
"""Example prompt_toolkit UIControl for displaying formatted text.
This is the prompt_toolkit class that is responsible for drawing the clock,
handling keybindings if in focus, and mouse input.
"""
def __init__(self, clock_pane: 'ClockPane', *args, **kwargs) -> None:
self.clock_pane = clock_pane
# Set some custom key bindings to toggle the view mode and wrap lines.
key_bindings = KeyBindings()
# If you press the v key this _toggle_view_mode function will be run.
@key_bindings.add('v')
def _toggle_view_mode(_event: KeyPressEvent) -> None:
"""Toggle view mode."""
self.clock_pane.toggle_view_mode()
# If you press the w key this _toggle_wrap_lines function will be run.
@key_bindings.add('w')
def _toggle_wrap_lines(_event: KeyPressEvent) -> None:
"""Toggle line wrapping."""
self.clock_pane.toggle_wrap_lines()
# Include the key_bindings keyword arg when passing to the parent class
# __init__ function.
kwargs['key_bindings'] = key_bindings
# Call the parent FormattedTextControl.__init__
super().__init__(*args, **kwargs)
def mouse_handler(self, mouse_event: MouseEvent):
"""Mouse handler for this control."""
# If the user clicks anywhere this function is run.
# Mouse positions relative to this control. x is the column starting
# from the left size as zero. y is the row starting with the top as
# zero.
_click_x = mouse_event.position.x
_click_y = mouse_event.position.y
# Mouse click behavior usually depends on if this window pane is in
# focus. If not in focus, then focus on it when left clicking. If
# already in focus then perform the action specific to this window.
# If not in focus, change focus to this clock pane and do nothing else.
if not has_focus(self.clock_pane)():
if mouse_event.event_type == MouseEventType.MOUSE_UP:
get_pw_console_app().focus_on_container(self.clock_pane)
# Mouse event handled, return None.
return None
# If code reaches this point, this window is already in focus.
# On left click
if mouse_event.event_type == MouseEventType.MOUSE_UP:
# Toggle the view mode.
self.clock_pane.toggle_view_mode()
# Mouse event handled, return None.
return None
# Mouse event not handled, return NotImplemented.
return NotImplemented
class ClockPane(WindowPane, PluginMixin):
"""Example Pigweed Console plugin window that displays a clock.
The ClockPane is a WindowPane based plugin that displays a clock and some
formatted text examples. It inherits from both WindowPane and
PluginMixin. It can be added on console startup by calling: ::
my_console.add_window_plugin(ClockPane())
For an example see:
https://pigweed.dev/pw_console/embedding.html#adding-plugins
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, pane_title='Clock', **kwargs)
# Some toggle settings to change view and wrap lines.
self.view_mode_clock: bool = True
self.wrap_lines: bool = False
# Counter variable to track how many times the background task runs.
self.background_task_update_count: int = 0
# ClockControl is responsible for rendering the dynamic content provided
# by self._get_formatted_text() and handle keyboard and mouse input.
# Using a control is always necessary for displaying any content that
# will change.
self.clock_control = ClockControl(
self, # This ClockPane class
self._get_formatted_text, # Callable to get text for display
# These are FormattedTextControl options.
# See the prompt_toolkit docs for all possible options
# https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.layout.FormattedTextControl
show_cursor=False,
focusable=True,
)
# Every FormattedTextControl object (ClockControl) needs to live inside
# a prompt_toolkit Window() instance. Here is where you specify
# alignment, style, and dimensions. See the prompt_toolkit docs for all
# opitons:
# https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.layout.Window
self.clock_control_window = Window(
# Set the content to the clock_control defined above.
content=self.clock_control,
# Make content left aligned
align=WindowAlign.LEFT,
# These two set to false make this window fill all available space.
dont_extend_width=False,
dont_extend_height=False,
# Content inside this window will have its lines wrapped if
# self.wrap_lines is True.
wrap_lines=Condition(lambda: self.wrap_lines),
)
# Create a toolbar for display at the bottom of this clock window. It
# will show the window title and buttons.
self.bottom_toolbar = WindowPaneToolbar(self)
# Add a button to toggle the view mode.
self.bottom_toolbar.add_button(
ToolbarButton(
key='v', # Key binding for this function
description='View Mode', # Button name
# Function to run when clicked.
mouse_handler=self.toggle_view_mode,
))
# Add a checkbox button to display if wrap_lines is enabled.
self.bottom_toolbar.add_button(
ToolbarButton(
key='w', # Key binding for this function
description='Wrap', # Button name
# Function to run when clicked.
mouse_handler=self.toggle_wrap_lines,
# Display a checkbox in this button.
is_checkbox=True,
# lambda that returns the state of the checkbox
checked=lambda: self.wrap_lines,
))
# self.container is the root container that contains objects to be
# rendered in the UI, one on top of the other.
self.container = self._create_pane_container(
# Display the clock window on top...
self.clock_control_window,
# and the bottom_toolbar below.
self.bottom_toolbar,
)
# This plugin needs to run a task in the background periodically and
# uses self.plugin_init() to set which function to run, and how often.
# This is provided by PluginMixin. See the docs for more info:
# https://pigweed.dev/pw_console/plugins.html#background-tasks
self.plugin_init(
plugin_callback=self._background_task,
# Run self._background_task once per second.
plugin_callback_frequency=1.0,
plugin_logger_name='pw_console_example_clock_plugin',
)
def _background_task(self) -> bool:
"""Function run in the background for the ClockPane plugin."""
self.background_task_update_count += 1
# Make a log message for debugging purposes. For more info see:
# https://pigweed.dev/pw_console/plugins.html#debugging-plugin-behavior
self.plugin_logger.debug('background_task_update_count: %s',
self.background_task_update_count)
# Returning True in the background task will force the user interface to
# re-draw.
# Returning False means no updates required.
return True
def toggle_view_mode(self):
"""Toggle the view mode between the clock and formatted text example."""
self.view_mode_clock = not self.view_mode_clock
self.redraw_ui()
def toggle_wrap_lines(self):
"""Enable or disable line wraping/truncation."""
self.wrap_lines = not self.wrap_lines
self.redraw_ui()
def _get_formatted_text(self):
"""This function returns the content that will be displayed in the user
interface depending on which view mode is active."""
if self.view_mode_clock:
return self._get_clock_text()
return self._get_example_text()
def _get_clock_text(self):
"""Create the time with some color formatting."""
# pylint: disable=no-self-use
# Get the date and time
date, time = datetime.now().isoformat(sep='_',
timespec='seconds').split('_')
# Formatted text is represented as (style, text) tuples.
# For more examples see:
# https://python-prompt-toolkit.readthedocs.io/en/latest/pages/printing_text.html
# These styles are selected using class names and start with the
# 'class:' prefix. For all classes defined by Pigweed Console see:
# https://cs.pigweed.dev/pigweed/+/main:pw_console/py/pw_console/style.py;l=189
# Date in cyan matching the current Pigweed Console theme.
date_with_color = ('class:theme-fg-cyan', date)
# Time in magenta
time_with_color = ('class:theme-fg-magenta', time)
# No color styles for line breaks and spaces.
line_break = ('', '\n')
space = ('', ' ')
# Concatenate the (style, text) tuples.
return FormattedText([
line_break,
space,
space,
date_with_color,
space,
time_with_color,
])
def _get_example_text(self):
"""Examples of how to create formatted text."""
# pylint: disable=no-self-use
# Make a list to hold all the formatted text to display.
fragments = []
# Some spacing vars
wide_space = ('', ' ')
space = ('', ' ')
newline = ('', '\n')
# HTML() is a shorthand way to style text. See:
# https://python-prompt-toolkit.readthedocs.io/en/latest/pages/printing_text.html#html
# This formats 'Foreground Colors' as underlined:
fragments.append(HTML('<u>Foreground Colors</u>\n'))
# Standard ANSI colors examples
fragments.append(
FormattedText([
# These tuples follow this format:
# (style_string, text_to_display)
('ansiblack', 'ansiblack'),
wide_space,
('ansired', 'ansired'),
wide_space,
('ansigreen', 'ansigreen'),
wide_space,
('ansiyellow', 'ansiyellow'),
wide_space,
('ansiblue', 'ansiblue'),
wide_space,
('ansimagenta', 'ansimagenta'),
wide_space,
('ansicyan', 'ansicyan'),
wide_space,
('ansigray', 'ansigray'),
wide_space,
newline,
('ansibrightblack', 'ansibrightblack'),
space,
('ansibrightred', 'ansibrightred'),
space,
('ansibrightgreen', 'ansibrightgreen'),
space,
('ansibrightyellow', 'ansibrightyellow'),
space,
('ansibrightblue', 'ansibrightblue'),
space,
('ansibrightmagenta', 'ansibrightmagenta'),
space,
('ansibrightcyan', 'ansibrightcyan'),
space,
('ansiwhite', 'ansiwhite'),
space,
]))
fragments.append(HTML('\n<u>Background Colors</u>\n'))
fragments.append(
FormattedText([
# Here's an example of a style that specifies both background
# and foreground colors. The background color is prefixed with
# 'bg:'. The foreground color follows that with no prefix.
('bg:ansiblack ansiwhite', 'ansiblack'),
wide_space,
('bg:ansired', 'ansired'),
wide_space,
('bg:ansigreen', 'ansigreen'),
wide_space,
('bg:ansiyellow', 'ansiyellow'),
wide_space,
('bg:ansiblue ansiwhite', 'ansiblue'),
wide_space,
('bg:ansimagenta', 'ansimagenta'),
wide_space,
('bg:ansicyan', 'ansicyan'),
wide_space,
('bg:ansigray', 'ansigray'),
wide_space,
('', '\n'),
('bg:ansibrightblack', 'ansibrightblack'),
space,
('bg:ansibrightred', 'ansibrightred'),
space,
('bg:ansibrightgreen', 'ansibrightgreen'),
space,
('bg:ansibrightyellow', 'ansibrightyellow'),
space,
('bg:ansibrightblue', 'ansibrightblue'),
space,
('bg:ansibrightmagenta', 'ansibrightmagenta'),
space,
('bg:ansibrightcyan', 'ansibrightcyan'),
space,
('bg:ansiwhite', 'ansiwhite'),
space,
]))
# These themes use Pigweed Console style classes. See full list in:
# https://cs.pigweed.dev/pigweed/+/main:pw_console/py/pw_console/style.py;l=189
fragments.append(HTML('\n\n<u>Current Theme Foreground Colors</u>\n'))
fragments.append([
('class:theme-fg-red', 'class:theme-fg-red'),
newline,
('class:theme-fg-orange', 'class:theme-fg-orange'),
newline,
('class:theme-fg-yellow', 'class:theme-fg-yellow'),
newline,
('class:theme-fg-green', 'class:theme-fg-green'),
newline,
('class:theme-fg-cyan', 'class:theme-fg-cyan'),
newline,
('class:theme-fg-blue', 'class:theme-fg-blue'),
newline,
('class:theme-fg-purple', 'class:theme-fg-purple'),
newline,
('class:theme-fg-magenta', 'class:theme-fg-magenta'),
newline,
])
fragments.append(HTML('\n<u>Current Theme Background Colors</u>\n'))
fragments.append([
('class:theme-bg-red', 'class:theme-bg-red'),
newline,
('class:theme-bg-orange', 'class:theme-bg-orange'),
newline,
('class:theme-bg-yellow', 'class:theme-bg-yellow'),
newline,
('class:theme-bg-green', 'class:theme-bg-green'),
newline,
('class:theme-bg-cyan', 'class:theme-bg-cyan'),
newline,
('class:theme-bg-blue', 'class:theme-bg-blue'),
newline,
('class:theme-bg-purple', 'class:theme-bg-purple'),
newline,
('class:theme-bg-magenta', 'class:theme-bg-magenta'),
newline,
])
fragments.append(HTML('\n<u>Theme UI Colors</u>\n'))
fragments.append([
('class:theme-fg-default', 'class:theme-fg-default'),
space,
('class:theme-bg-default', 'class:theme-bg-default'),
space,
('class:theme-bg-active', 'class:theme-bg-active'),
space,
('class:theme-fg-active', 'class:theme-fg-active'),
space,
('class:theme-bg-inactive', 'class:theme-bg-inactive'),
space,
('class:theme-fg-inactive', 'class:theme-fg-inactive'),
newline,
('class:theme-fg-dim', 'class:theme-fg-dim'),
space,
('class:theme-bg-dim', 'class:theme-bg-dim'),
space,
('class:theme-bg-dialog', 'class:theme-bg-dialog'),
space,
('class:theme-bg-line-highlight', 'class:theme-bg-line-highlight'),
space,
('class:theme-bg-button-active', 'class:theme-bg-button-active'),
space,
('class:theme-bg-button-inactive',
'class:theme-bg-button-inactive'),
space,
])
# Return all formatted text lists merged together.
return merge_formatted_text(fragments)