pw_console: Remove helpers.py
Move helper.py functions into better locations. This CL is only a
refactor, no new functionality.
New files and their functions.
- text_formatting.py
- remove_formatting
- strip_ansi
- get_line_height
- widgets/checkbox.py
- to_checkbox
- to_checkbox_text
- widgets/focus_on_click_overlay.py
- FocusOnClickFloatContainer
- create_overlay
- style.py
- get_pane_style
- get_pane_indicator
- get_toolbar_style
No-Docs-Update-Reason: Refactor
Change-Id: I145bea0758eec599c00f2d399286ea130f07c694
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/51700
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Pigweed-Auto-Submit: Anthony DiGirolamo <tonymd@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_console/py/BUILD.gn b/pw_console/py/BUILD.gn
index ef1a54a..f3f4e9a 100644
--- a/pw_console/py/BUILD.gn
+++ b/pw_console/py/BUILD.gn
@@ -23,19 +23,23 @@
"pw_console/__main__.py",
"pw_console/console_app.py",
"pw_console/help_window.py",
- "pw_console/helpers.py",
"pw_console/key_bindings.py",
"pw_console/log_container.py",
"pw_console/log_pane.py",
+ "pw_console/mouse.py",
"pw_console/pw_ptpython_repl.py",
"pw_console/repl_pane.py",
"pw_console/style.py",
+ "pw_console/text_formatting.py",
+ "pw_console/widgets/__init__.py",
+ "pw_console/widgets/checkbox.py",
+ "pw_console/widgets/focus_on_click_overlay.py",
]
tests = [
"console_app_test.py",
"help_window_test.py",
- "helpers_test.py",
"repl_pane_test.py",
+ "text_formatting_test.py",
]
python_deps = [
"$dir_pw_cli/py",
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py
index e864cf9..fc5789b 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -47,7 +47,7 @@
)
import pw_console.key_bindings
-import pw_console.helpers
+import pw_console.widgets.checkbox
import pw_console.style
from pw_console.help_window import HelpWindow
from pw_console.log_pane import LogPane
@@ -307,13 +307,13 @@
'[Window]',
children=[
MenuItem('{check} Vertical Window Spliting'.format(
- check=pw_console.helpers.to_checkbox_text(
+ check=pw_console.widgets.checkbox.to_checkbox_text(
self.vertical_split)),
handler=self.toggle_vertical_split),
MenuItem('-'),
] + [
MenuItem('{check} {index}: {title} {subtitle}'.format(
- check=pw_console.helpers.to_checkbox_text(
+ check=pw_console.widgets.checkbox.to_checkbox_text(
pane.show_pane),
index=index + 1,
title=pane.pane_title(),
diff --git a/pw_console/py/pw_console/helpers.py b/pw_console/py/pw_console/helpers.py
deleted file mode 100644
index b8ac373..0000000
--- a/pw_console/py/pw_console/helpers.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# 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.
-"""Helper functions."""
-
-from prompt_toolkit.filters import has_focus
-
-
-def remove_formatting(formatted_text):
- """Throw away style info from formatted text tuples."""
- return ''.join([formatted_tuple[1] for formatted_tuple in formatted_text]) # pylint: disable=not-an-iterable
-
-
-def get_line_height(text_width, screen_width, prefix_width):
- """Calculates line height for a string with line wrapping enabled."""
- if text_width == 0:
- return 0
-
- # If text will fit on the screen without wrapping.
- if text_width <= screen_width:
- return 1, screen_width - text_width
-
- # Assume zero width prefix if it's >= width of the screen.
- if prefix_width >= screen_width:
- prefix_width = 0
-
- # Start with height of 1 row.
- total_height = 1
-
- # One screen_width of characters (with no prefix) is displayed first.
- remaining_width = text_width - screen_width
-
- # While we have caracters remaining to be displayed
- while remaining_width > 0:
- # Add the new indentation prefix
- remaining_width += prefix_width
- # Display this line
- remaining_width -= screen_width
- # Add a line break
- total_height += 1
-
- # Remaining characters is what's left below zero.
- return (total_height, abs(remaining_width))
-
-
-def get_toolbar_style(pt_container) -> str:
- """Return the style class for a toolbar if pt_container is in focus."""
- if has_focus(pt_container.__pt_container__())():
- return 'class:toolbar_active'
- return 'class:toolbar_inactive'
-
-
-def get_pane_style(pt_container) -> str:
- """Return the style class for a pane title if pt_container is in focus."""
- if has_focus(pt_container.__pt_container__())():
- return 'class:pane_active'
- return 'class:pane_inactive'
-
-
-def get_pane_indicator(pt_container, title, mouse_handler=None):
- """Return formatted text for a pane indicator and title."""
- if mouse_handler:
- inactive_indicator = ('class:pane_indicator_inactive', ' ',
- mouse_handler)
- active_indicator = ('class:pane_indicator_active', ' ', mouse_handler)
- inactive_title = ('class:pane_title_inactive', title, mouse_handler)
- active_title = ('class:pane_title_active', title, mouse_handler)
- else:
- inactive_indicator = ('class:pane_indicator_inactive', ' ')
- active_indicator = ('class:pane_indicator_active', ' ')
- inactive_title = ('class:pane_title_inactive', title)
- active_title = ('class:pane_title_active', title)
-
- if has_focus(pt_container.__pt_container__())():
- return [active_indicator, active_title]
- return [inactive_indicator, inactive_title]
-
-
-def to_checkbox(checked: bool, mouse_handler=None):
- default_style = 'class:checkbox'
- checked_style = 'class:checkbox-checked'
- text = '[x] ' if checked else '[ ] '
- style = checked_style if checked else default_style
- if mouse_handler:
- return (style, text, mouse_handler)
- return (style, text)
-
-
-def to_checkbox_text(checked: bool):
- return to_checkbox(checked)[1]
diff --git a/pw_console/py/pw_console/log_container.py b/pw_console/py/pw_console/log_container.py
index cd92ef3..482473d 100644
--- a/pw_console/py/pw_console/log_container.py
+++ b/pw_console/py/pw_console/log_container.py
@@ -15,7 +15,6 @@
import collections
import logging
-import re
import sys
import time
from dataclasses import dataclass
@@ -32,12 +31,10 @@
import pw_cli.color
from pw_log_tokenized import FormatStringWithMetadata
-import pw_console.helpers
+import pw_console.text_formatting
_LOG = logging.getLogger(__package__)
-_ANSI_SEQUENCE_REGEX = re.compile(r'\x1b[^m]*m')
-
@dataclass
class LogLine:
@@ -216,8 +213,9 @@
asctime=record.asctime, levelname=record.levelname)
# Delete ANSI escape sequences.
- ansi_stripped_time_and_level = _ANSI_SEQUENCE_REGEX.sub(
- '', formatted_time_and_level)
+ ansi_stripped_time_and_level = (
+ pw_console.text_formatting.strip_ansi(formatted_time_and_level)
+ )
self.channel_formatted_prefix_widths[record.name] = len(
ansi_stripped_time_and_level)
@@ -230,7 +228,8 @@
"""Add a new log event."""
# Format incoming log line.
formatted_log = self.format(record)
- ansi_stripped_log = _ANSI_SEQUENCE_REGEX.sub('', formatted_log)
+ ansi_stripped_log = pw_console.text_formatting.strip_ansi(
+ formatted_log)
# Save this log.
self.logs.append(
LogLine(record=record,
@@ -438,7 +437,7 @@
remaining_width = 0
if self.wrap_lines_enabled() and (fragment_width > window_width):
line_height, remaining_width = (
- pw_console.helpers.get_line_height(
+ pw_console.text_formatting.get_line_height(
fragment_width, window_width,
self.get_line_wrap_prefix_width()))
diff --git a/pw_console/py/pw_console/log_pane.py b/pw_console/py/pw_console/log_pane.py
index 2f1ec8c..5d2b4f9 100644
--- a/pw_console/py/pw_console/log_pane.py
+++ b/pw_console/py/pw_console/log_pane.py
@@ -40,7 +40,8 @@
from prompt_toolkit.layout.dimension import AnyDimension
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
-import pw_console.helpers
+import pw_console.widgets.checkbox
+import pw_console.style
from pw_console.log_container import LogContainer
@@ -115,8 +116,8 @@
title = ' {} '.format(log_pane.pane_title())
mouse_handler = functools.partial(
LogPaneBottomToolbarBar.mouse_handler_focus, log_pane)
- return pw_console.helpers.get_pane_indicator(log_pane, title,
- mouse_handler)
+ return pw_console.style.get_pane_indicator(log_pane, title,
+ mouse_handler)
@staticmethod
def get_center_text_tokens(log_pane):
@@ -134,34 +135,38 @@
LogPaneBottomToolbarBar.mouse_handler_toggle_follow, log_pane)
# FormattedTextTuple contents: (Style, Text, Mouse handler)
- separator_text = ('', ' ') # 1 space of separaton between keybinds.
+ separator_text = ('', ' ') # 2 space of separaton between keybinds.
- # If the log_pane is in focus, show keybinds in the toolbar.
- if has_focus(log_pane.__pt_container__())():
- return [
- separator_text,
- pw_console.helpers.to_checkbox(log_pane.wrap_lines,
- toggle_wrap_lines),
- ('class:keybind', 'w', toggle_wrap_lines),
- ('class:keyhelp', ':Wrap', toggle_wrap_lines),
- separator_text,
- pw_console.helpers.to_checkbox(log_pane.log_container.follow,
- toggle_follow),
- ('class:keybind', 'f', toggle_follow),
- ('class:keyhelp', ':Follow', toggle_follow),
- separator_text,
- ('class:keybind', 'C', clear_history),
- ('class:keyhelp', ':Clear', clear_history),
- ]
# Show the click to focus button if log pane isn't in focus.
return [
- ('class:keyhelp', '[click to focus] ', focus),
+ separator_text,
+ pw_console.widgets.checkbox.to_checkbox(log_pane.wrap_lines,
+ toggle_wrap_lines,
+ end=''),
+ ('class:keyhelp', 'Wrap:', toggle_wrap_lines),
+ ('class:keybind', 'w', toggle_wrap_lines),
+ separator_text,
+ pw_console.widgets.checkbox.to_checkbox(
+ log_pane.log_container.follow, toggle_follow, end=''),
+ ('class:keyhelp', 'Follow:', toggle_follow),
+ ('class:keybind', 'f', toggle_follow),
+ separator_text,
+ ('class:keyhelp', 'Clear:', clear_history),
+ ('class:keybind', 'C', clear_history),
+ # Remaining whitespace should focus on click.
+ ('class:keybind', ' ', focus),
]
@staticmethod
def get_right_text_tokens(log_pane):
"""Return formatted text tokens for display."""
- return [('', ' {} '.format(log_pane.pane_subtitle()))]
+ focus = functools.partial(LogPaneBottomToolbarBar.mouse_handler_focus,
+ log_pane)
+ fragments = []
+ if not has_focus(log_pane.__pt_container__())():
+ fragments.append(('class:keyhelp', '[click to focus] ', focus))
+ fragments.append(('', ' {} '.format(log_pane.pane_subtitle())))
+ return fragments
def __init__(self, log_pane):
title_section_window = Window(
@@ -199,7 +204,7 @@
log_source_name,
],
height=LogPaneBottomToolbarBar.TOOLBAR_HEIGHT,
- style=functools.partial(pw_console.helpers.get_toolbar_style,
+ style=functools.partial(pw_console.style.get_toolbar_style,
log_pane),
align=WindowAlign.LEFT,
)
@@ -427,7 +432,7 @@
dont_extend_width=False,
# Needed for log lines ANSI sequences that don't specify foreground
# or background colors.
- style=functools.partial(pw_console.helpers.get_pane_style, self),
+ style=functools.partial(pw_console.style.get_pane_style, self),
)
# Root level container
@@ -444,7 +449,7 @@
align=VerticalAlign.BOTTOM,
height=self.height,
width=self.width,
- style=functools.partial(pw_console.helpers.get_pane_style,
+ style=functools.partial(pw_console.style.get_pane_style,
self),
),
floats=[
diff --git a/pw_console/py/pw_console/mouse.py b/pw_console/py/pw_console/mouse.py
new file mode 100644
index 0000000..c40897a
--- /dev/null
+++ b/pw_console/py/pw_console/mouse.py
@@ -0,0 +1,27 @@
+# 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.
+"""Mouse handler fuctions."""
+
+from prompt_toolkit.application.current import get_app
+from prompt_toolkit.filters import has_focus
+from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
+
+
+def focus_handler(container, mouse_event: MouseEvent):
+ """Focus container on click."""
+ if not has_focus(container)():
+ if mouse_event.event_type == MouseEventType.MOUSE_UP:
+ get_app().layout.focus(container)
+ return None
+ return NotImplemented
diff --git a/pw_console/py/pw_console/pw_ptpython_repl.py b/pw_console/py/pw_console/pw_ptpython_repl.py
index e3e7674..ea5b6fe 100644
--- a/pw_console/py/pw_console/pw_ptpython_repl.py
+++ b/pw_console/py/pw_console/pw_ptpython_repl.py
@@ -28,7 +28,7 @@
from ptpython.completer import CompletePrivateAttributes # type: ignore
-import pw_console.helpers
+import pw_console.text_formatting
_LOG = logging.getLogger(__package__)
@@ -79,7 +79,7 @@
def _save_result(self, formatted_text):
"""Save the last repl execution result."""
- unformatted_result = pw_console.helpers.remove_formatting(
+ unformatted_result = pw_console.text_formatting.remove_formatting(
formatted_text)
self._last_result = unformatted_result
@@ -127,7 +127,8 @@
if result_value is not None:
formatted_result = self._format_result_output(result_value)
- result = pw_console.helpers.remove_formatting(formatted_result)
+ result = pw_console.text_formatting.remove_formatting(
+ formatted_result)
# Job is finished, append the last result.
self.repl_pane.append_result_to_executed_code(input_text, future,
diff --git a/pw_console/py/pw_console/repl_pane.py b/pw_console/py/pw_console/repl_pane.py
index a25d95c..194a4d1 100644
--- a/pw_console/py/pw_console/repl_pane.py
+++ b/pw_console/py/pw_console/repl_pane.py
@@ -32,13 +32,11 @@
has_focus,
)
from prompt_toolkit.document import Document
-from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
from prompt_toolkit.layout.dimension import AnyDimension
from prompt_toolkit.widgets import TextArea
from prompt_toolkit.layout import (
ConditionalContainer,
Dimension,
- Float,
FloatContainer,
FormattedTextControl,
HSplit,
@@ -49,11 +47,9 @@
from prompt_toolkit.lexers import PygmentsLexer # type: ignore
from pygments.lexers.python import PythonLexer # type: ignore
-from pw_console.helpers import (
- get_pane_indicator,
- get_pane_style,
- get_toolbar_style,
-)
+import pw_console.mouse
+import pw_console.style
+import pw_console.widgets.focus_on_click_overlay
from pw_console.pw_ptpython_repl import PwPtPythonRepl
_LOG = logging.getLogger(__package__)
@@ -67,38 +63,6 @@
OUTPUT_TEMPLATE = tmpl.read()
-def mouse_focus_handler(repl_pane, mouse_event: MouseEvent):
- """Focus the repl_pane on click."""
- if not has_focus(repl_pane)():
- if mouse_event.event_type == MouseEventType.MOUSE_UP:
- repl_pane.application.application.layout.focus(repl_pane)
- return None
- return NotImplemented
-
-
-class FocusOnClickFloatContainer(ConditionalContainer):
- """Empty container rendered if the repl_pane is not in focus.
-
- This container should be rendered with transparent=True so nothing is shown
- to the user. Container is not rendered if the repl_pane is already in focus.
- """
- def __init__(self, repl_pane):
-
- empty_text = FormattedTextControl([(
- # Style
- 'class:pane_inactive',
- # Text
- ' ',
- # Mouse handler
- functools.partial(mouse_focus_handler, repl_pane),
- )])
-
- super().__init__(
- Window(empty_text),
- filter=Condition(lambda: not has_focus(repl_pane)()),
- )
-
-
class ReplPaneBottomToolbarBar(ConditionalContainer):
"""Repl pane bottom toolbar."""
@staticmethod
@@ -106,8 +70,10 @@
"""Return toolbar indicator and title."""
title = ' Python Input '
- mouse_handler = functools.partial(mouse_focus_handler, repl_pane)
- return get_pane_indicator(repl_pane, title, mouse_handler)
+ mouse_handler = functools.partial(pw_console.mouse.focus_handler,
+ repl_pane)
+ return pw_console.style.get_pane_indicator(repl_pane, title,
+ mouse_handler)
@staticmethod
def get_center_text_tokens(repl_pane):
@@ -119,24 +85,15 @@
# Text
' ',
# Mouse handler
- functools.partial(mouse_focus_handler, repl_pane),
+ functools.partial(pw_console.mouse.focus_handler, repl_pane),
),
('class:keybind', 'enter'),
('class:keyhelp', ':Run code'),
]
- out_of_focus_text = [(
- # Style
- 'class:keyhelp',
- # Text
- '[click to focus] ',
- # Mouse handler
- functools.partial(mouse_focus_handler, repl_pane),
- )]
-
if has_focus(repl_pane)():
return focused_text
- return out_of_focus_text
+ return [('', '')]
@staticmethod
def get_right_text_tokens(repl_pane):
@@ -148,7 +105,15 @@
('class:keybind', 'F3'),
('class:keyhelp', ':History '),
]
- return []
+
+ return [(
+ # Style
+ 'class:keyhelp',
+ # Text
+ '[click to focus] ',
+ # Mouse handler
+ functools.partial(pw_console.mouse.focus_handler, repl_pane),
+ )]
def __init__(self, repl_pane):
left_section_window = Window(
@@ -192,7 +157,8 @@
right_section_window,
],
height=1,
- style=functools.partial(get_toolbar_style, repl_pane),
+ style=functools.partial(pw_console.style.get_toolbar_style,
+ repl_pane),
align=WindowAlign.LEFT,
)
@@ -273,6 +239,7 @@
Window(
content=FormattedTextControl(
functools.partial(
+ pw_console.style.
get_pane_indicator, self,
' Python Results ')),
align=WindowAlign.LEFT,
@@ -281,7 +248,7 @@
),
],
style=functools.partial(
- get_toolbar_style, self),
+ pw_console.style.get_toolbar_style, self),
),
]),
HSplit([
@@ -293,25 +260,14 @@
],
height=self.height,
width=self.width,
- style=functools.partial(get_pane_style, self),
+ style=functools.partial(pw_console.style.get_pane_style,
+ self),
),
floats=[
- # Transparent float container that will focus on the
- # repl_pane when clicked. It is hidden if already in focus.
- Float(
- # This is drawn as the full size of the ReplPane
- FocusOnClickFloatContainer(self),
- transparent=True,
- # Draw the empty space in the bottom right corner.
- # Distance to the right edge
- right=1,
- # Distance to the bottom edge
- bottom=1,
- # Don't specify left or top to 0, it would override
- # right+bottom and move it to the top left.
- # left=0,
- # top=0,
- ),
+ # Transparent float container that will focus on this
+ # ReplPane when clicked.
+ pw_console.widgets.focus_on_click_overlay.create_overlay(
+ self),
]),
filter=Condition(lambda: self.show_pane))
diff --git a/pw_console/py/pw_console/style.py b/pw_console/py/pw_console/style.py
index 98164aa..5c80bde 100644
--- a/pw_console/py/pw_console/style.py
+++ b/pw_console/py/pw_console/style.py
@@ -17,6 +17,7 @@
from dataclasses import dataclass
from prompt_toolkit.styles import Style
+from prompt_toolkit.filters import has_focus
_LOG = logging.getLogger(__package__)
@@ -149,3 +150,36 @@
} # yapf: disable
return Style.from_dict(pw_console_styles)
+
+
+def get_toolbar_style(pt_container) -> str:
+ """Return the style class for a toolbar if pt_container is in focus."""
+ if has_focus(pt_container.__pt_container__())():
+ return 'class:toolbar_active'
+ return 'class:toolbar_inactive'
+
+
+def get_pane_style(pt_container) -> str:
+ """Return the style class for a pane title if pt_container is in focus."""
+ if has_focus(pt_container.__pt_container__())():
+ return 'class:pane_active'
+ return 'class:pane_inactive'
+
+
+def get_pane_indicator(pt_container, title, mouse_handler=None):
+ """Return formatted text for a pane indicator and title."""
+ if mouse_handler:
+ inactive_indicator = ('class:pane_indicator_inactive', ' ',
+ mouse_handler)
+ active_indicator = ('class:pane_indicator_active', ' ', mouse_handler)
+ inactive_title = ('class:pane_title_inactive', title, mouse_handler)
+ active_title = ('class:pane_title_active', title, mouse_handler)
+ else:
+ inactive_indicator = ('class:pane_indicator_inactive', ' ')
+ active_indicator = ('class:pane_indicator_active', ' ')
+ inactive_title = ('class:pane_title_inactive', title)
+ active_title = ('class:pane_title_active', title)
+
+ if has_focus(pt_container.__pt_container__())():
+ return [active_indicator, active_title]
+ return [inactive_indicator, inactive_title]
diff --git a/pw_console/py/pw_console/text_formatting.py b/pw_console/py/pw_console/text_formatting.py
new file mode 100644
index 0000000..d818893
--- /dev/null
+++ b/pw_console/py/pw_console/text_formatting.py
@@ -0,0 +1,60 @@
+# 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.
+"""Text formatting functions."""
+
+import re
+
+_ANSI_SEQUENCE_REGEX = re.compile(r'\x1b[^m]*m')
+
+
+def strip_ansi(text: str):
+ """Strip out ANSI escape sequences."""
+ return _ANSI_SEQUENCE_REGEX.sub('', text)
+
+
+def remove_formatting(formatted_text):
+ """Throw away style info from prompt_toolkit formatted text tuples."""
+ return ''.join([formatted_tuple[1] for formatted_tuple in formatted_text]) # pylint: disable=not-an-iterable
+
+
+def get_line_height(text_width, screen_width, prefix_width):
+ """Calculates line height for a string with line wrapping enabled."""
+ if text_width == 0:
+ return 0
+
+ # If text will fit on the screen without wrapping.
+ if text_width <= screen_width:
+ return 1, screen_width - text_width
+
+ # Assume zero width prefix if it's >= width of the screen.
+ if prefix_width >= screen_width:
+ prefix_width = 0
+
+ # Start with height of 1 row.
+ total_height = 1
+
+ # One screen_width of characters (with no prefix) is displayed first.
+ remaining_width = text_width - screen_width
+
+ # While we have caracters remaining to be displayed
+ while remaining_width > 0:
+ # Add the new indentation prefix
+ remaining_width += prefix_width
+ # Display this line
+ remaining_width -= screen_width
+ # Add a line break
+ total_height += 1
+
+ # Remaining characters is what's left below zero.
+ return (total_height, abs(remaining_width))
diff --git a/pw_console/py/pw_console/widgets/__init__.py b/pw_console/py/pw_console/widgets/__init__.py
new file mode 100644
index 0000000..62a3c11
--- /dev/null
+++ b/pw_console/py/pw_console/widgets/__init__.py
@@ -0,0 +1,14 @@
+# 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.
+"""Pigweed Console UI widgets."""
diff --git a/pw_console/py/pw_console/widgets/checkbox.py b/pw_console/py/pw_console/widgets/checkbox.py
new file mode 100644
index 0000000..d9e873c
--- /dev/null
+++ b/pw_console/py/pw_console/widgets/checkbox.py
@@ -0,0 +1,29 @@
+# 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.
+"""Functions to create checkboxes for menus and toolbars."""
+
+
+def to_checkbox(checked: bool, mouse_handler=None, end=' '):
+ default_style = 'class:checkbox'
+ checked_style = 'class:checkbox-checked'
+ text = '[x]' if checked else '[ ]'
+ text += end
+ style = checked_style if checked else default_style
+ if mouse_handler:
+ return (style, text, mouse_handler)
+ return (style, text)
+
+
+def to_checkbox_text(checked: bool):
+ return to_checkbox(checked)[1]
diff --git a/pw_console/py/pw_console/widgets/focus_on_click_overlay.py b/pw_console/py/pw_console/widgets/focus_on_click_overlay.py
new file mode 100644
index 0000000..8d7f41c
--- /dev/null
+++ b/pw_console/py/pw_console/widgets/focus_on_click_overlay.py
@@ -0,0 +1,79 @@
+# 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.
+"""Float container that will focus on a given container on click."""
+
+import functools
+
+from prompt_toolkit.filters import (
+ Condition,
+ has_focus,
+)
+from prompt_toolkit.layout import (
+ ConditionalContainer,
+ Float,
+ FormattedTextControl,
+ Window,
+ WindowAlign,
+)
+
+import pw_console.mouse
+
+
+class FocusOnClickFloatContainer(ConditionalContainer):
+ """Empty container rendered if the repl_pane is not in focus.
+
+ This container should be rendered with transparent=True so nothing is shown
+ to the user. Container is not rendered if the repl_pane is already in focus.
+ """
+ def __init__(self, target_container):
+
+ empty_text = FormattedTextControl([(
+ 'class:pane_inactive', # Style
+ # Text here must be a printable character or the mouse handler won't
+ # work.
+ ' ',
+ functools.partial(pw_console.mouse.focus_handler,
+ target_container), # Mouse handler
+ )])
+
+ super().__init__(
+ Window(
+ empty_text,
+ # Draw the empty space in the upper right corner
+ align=WindowAlign.RIGHT,
+ # Make sure window fills all available space.
+ dont_extend_width=False,
+ dont_extend_height=False,
+ ),
+ filter=Condition(lambda: not has_focus(target_container)()),
+ )
+
+
+def create_overlay(target_container):
+ """Create a transparent FocusOnClickFloatContainer.
+
+ The target_container will be focused when clicked. The overlay float will be
+ hidden if target_container is already in focus.
+ """
+ return Float(
+ # This is drawn as the full size of the ReplPane
+ FocusOnClickFloatContainer(target_container),
+ transparent=True,
+ # Draw the empty space in the bottom right corner.
+ # Distance to each edge, fill the whole container.
+ left=0,
+ top=0,
+ right=0,
+ bottom=0,
+ )
diff --git a/pw_console/py/helpers_test.py b/pw_console/py/text_formatting_test.py
similarity index 97%
rename from pw_console/py/helpers_test.py
rename to pw_console/py/text_formatting_test.py
index 435141f..600b5ea 100644
--- a/pw_console/py/helpers_test.py
+++ b/pw_console/py/text_formatting_test.py
@@ -11,12 +11,12 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""Tests for pw_console.console_app"""
+"""Tests for pw_console.text_formatting"""
import unittest
from parameterized import parameterized # type: ignore
-from pw_console.helpers import get_line_height
+from pw_console.text_formatting import get_line_height
class TestHelperFunctions(unittest.TestCase):