blob: 89689ea624fa0ed5add813b1dee5a823793267b6 [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.
"""Help window container class."""
import logging
import inspect
from pathlib import Path
from typing import Dict
from jinja2 import Template
from prompt_toolkit.document import Document
from prompt_toolkit.filters import Condition
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import (
ConditionalContainer,
DynamicContainer,
HSplit,
)
from prompt_toolkit.layout.dimension import Dimension
from prompt_toolkit.widgets import Box, Frame, TextArea
_LOG = logging.getLogger(__package__)
HELP_TEMPLATE_PATH = Path(__file__).parent / "templates" / "keybind_list.jinja"
with HELP_TEMPLATE_PATH.open() as tmpl:
KEYBIND_TEMPLATE = tmpl.read()
class HelpWindow(ConditionalContainer):
"""Help window container for displaying keybindings."""
def __init__(self, application, preamble='', additional_help_text=''):
# Dict containing key = section title and value = list of key bindings.
self.help_text_sections = {}
self.max_description_width = 0
self.max_key_list_width = 0
self.max_line_length = 0
# Generated keybinding text
self.preamble = preamble
self.additional_help_text = additional_help_text
self.help_text = ''
self.help_text_area = TextArea(
focusable=True,
scrollbar=True,
style='class:help_window_content',
)
frame = Frame(
body=Box(
body=DynamicContainer(lambda: self.help_text_area),
padding=Dimension(preferred=1, max=1),
padding_bottom=0,
padding_top=0,
char=' ',
style='class:frame.border', # Same style used for Frame.
), )
super().__init__(
HSplit([frame]),
filter=Condition(lambda: application.show_help_window),
)
def content_width(self) -> int:
"""Return total width of help window."""
# Widths of UI elements
frame_width = 1
padding_width = 1
left_side_frame_and_padding_width = frame_width + padding_width
right_side_frame_and_padding_width = frame_width + padding_width
scrollbar_padding = 1
scrollbar_width = 1
return self.max_line_length + (left_side_frame_and_padding_width +
right_side_frame_and_padding_width +
scrollbar_padding + scrollbar_width)
def generate_help_text(self):
"""Generate help text based on added key bindings."""
# pylint: disable=line-too-long
template = Template(
KEYBIND_TEMPLATE,
trim_blocks=True,
lstrip_blocks=True,
)
self.help_text = template.render(
sections=self.help_text_sections,
max_description_width=self.max_description_width,
max_key_list_width=self.max_key_list_width,
preamble=self.preamble,
additional_help_text=self.additional_help_text,
)
# Find the longest line in the rendered template.
self.max_line_length = 0
for line in self.help_text.splitlines():
if len(line) > self.max_line_length:
self.max_line_length = len(line)
# Replace the TextArea content.
self.help_text_area.buffer.document = Document(text=self.help_text,
cursor_position=0)
return self.help_text
def add_custom_keybinds_help_text(self, section_name, key_bindings: Dict):
"""Add hand written key_bindings."""
self.help_text_sections[section_name] = key_bindings
def add_keybind_help_text(self, section_name, key_bindings: KeyBindings):
"""Append formatted key binding text to this help window."""
# Create a new keybind section, erasing any old section with thesame
# title.
self.help_text_sections[section_name] = {}
# Loop through passed in prompt_toolkit key_bindings.
for binding in key_bindings.bindings:
# Skip this keybind if the method name ends in _hidden.
if binding.handler.__name__.endswith('_hidden'):
continue
# Get the key binding description from the function doctstring.
docstring = binding.handler.__doc__
if not docstring:
docstring = ''
description = inspect.cleandoc(docstring)
description = description.replace('\n', ' ')
# Save the length of the description.
if len(description) > self.max_description_width:
self.max_description_width = len(description)
# Get the existing list of keys for this function or make a new one.
key_list = self.help_text_sections[section_name].get(
description, list())
# Save the name of the key e.g. F1, q, ControlQ, ControlUp
key_name = '-'.join(
[getattr(key, 'name', str(key)) for key in binding.keys])
key_name = key_name.replace('Control', 'Ctrl-')
key_name = key_name.replace('Shift', 'Shift-')
key_name = key_name.replace('Escape-', 'Alt-')
key_list.append(key_name)
key_list_width = len(', '.join(key_list))
# Save the length of the key list.
if key_list_width > self.max_key_list_width:
self.max_key_list_width = key_list_width
# Update this functions key_list
self.help_text_sections[section_name][description] = key_list