feat(python): add debuglink helpers to get layout information

pull/2643/head
grdddj 2 years ago committed by Martin Milata
parent 2cbb9efeca
commit e9a1bcc951

@ -15,8 +15,8 @@
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
import logging
import re
import textwrap
from collections import namedtuple
from copy import deepcopy
from datetime import datetime
from enum import IntEnum
@ -56,13 +56,129 @@ if TYPE_CHECKING:
EXPECTED_RESPONSES_CONTEXT_LINES = 3
LayoutLines = namedtuple("LayoutLines", "lines text")
LOG = logging.getLogger(__name__)
def layout_lines(lines: Sequence[str]) -> LayoutLines:
return LayoutLines(lines, " ".join(lines))
class LayoutContent:
"""Stores content of a layout as returned from Trezor.
Contains helper functions to extract specific parts of the layout.
"""
def __init__(self, lines: Sequence[str]) -> None:
self.lines = list(lines)
self.text = " ".join(self.lines)
def get_title(self) -> str:
"""Get title of the layout.
Title is located between "title" and "content" identifiers.
Example: "< Frame title : RECOVERY SHARE #1 content : < SwipePage"
-> "RECOVERY SHARE #1"
"""
match = re.search(r"title : (.*?) content :", self.text)
if not match:
return ""
return match.group(1).strip()
def get_content(self, tag_name: str = "Paragraphs", raw: bool = False) -> str:
"""Get text of the main screen content of the layout."""
content = "".join(self._get_content_lines(tag_name, raw))
if not raw and content.endswith(" "):
# Stripping possible space at the end
content = content[:-1]
return content
def get_button_texts(self) -> List[str]:
"""Get text of all buttons in the layout.
Example button: "< Button text : LADYBUG >"
-> ["LADYBUG"]
"""
return re.findall(r"< Button text : +(.*?) >", self.text)
def get_seed_words(self) -> List[str]:
"""Get all the seed words on the screen in order.
Example content: "1. ladybug 2. acid 3. academic 4. afraid"
-> ["ladybug", "acid", "academic", "afraid"]
"""
return re.findall(r"\d+\. (\w+)\b", self.get_content())
def get_page_count(self) -> int:
"""Get number of pages for the layout."""
return self._get_number("page_count")
def get_active_page(self) -> int:
"""Get current page index of the layout."""
return self._get_number("active_page")
def _get_number(self, key: str) -> int:
"""Get number connected with a specific key."""
match = re.search(rf"{key} : +(\d+)", self.text)
if not match:
return 0
return int(match.group(1))
def _get_content_lines(
self, tag_name: str = "Paragraphs", raw: bool = False
) -> List[str]:
"""Get lines of the main screen content of the layout."""
# First line should have content after the tag, last line does not store content
tag = f"< {tag_name}"
if tag in self.lines[0]:
first_line = self.lines[0].split(tag)[1]
all_lines = [first_line] + self.lines[1:-1]
else:
all_lines = self.lines[1:-1]
if raw:
return all_lines
else:
return [_clean_line(line) for line in all_lines]
def _clean_line(line: str) -> str:
"""Cleaning the layout line for extra spaces, hyphens and ellipsis.
Line usually comes in the form of " <content> ", with trailing spaces
at both ends. It may end with a hyphen (" - ") or ellipsis (" ... ").
Hyphen means the word was split to the next line, ellipsis signals
the text continuing on the next page.
"""
# Deleting space at the beginning
if line.startswith(" "):
line = line[1:]
# Deleting a hyphen at the end, together with the space
# before it, so it will be tightly connected with the next line
if line.endswith(" - "):
line = line[:-3]
# Deleting the ellipsis at the end (but preserving the space there)
if line.endswith(" ... "):
line = line[:-4]
return line
def multipage_content(layouts: List[LayoutContent]) -> str:
"""Get overall content from multiple-page layout."""
final_text = ""
for layout in layouts:
final_text += layout.get_content()
# When the raw content of the page ends with ellipsis,
# we need to add a space to separate it with the next page
if layout.get_content(raw=True).endswith("... "):
final_text += " "
# Stripping possible space at the end of last page
if final_text.endswith(" "):
final_text = final_text[:-1]
return final_text
class DebugLink:
@ -114,14 +230,14 @@ class DebugLink:
def state(self) -> messages.DebugLinkState:
return self._call(messages.DebugLinkGetState())
def read_layout(self) -> LayoutLines:
return layout_lines(self.state().layout_lines)
def read_layout(self) -> LayoutContent:
return LayoutContent(self.state().layout_lines)
def wait_layout(self) -> LayoutLines:
def wait_layout(self) -> LayoutContent:
obj = self._call(messages.DebugLinkGetState(wait_layout=True))
if isinstance(obj, messages.Failure):
raise TrezorFailure(obj)
return layout_lines(obj.layout_lines)
return LayoutContent(obj.layout_lines)
def watch_layout(self, watch: bool) -> None:
"""Enable or disable watching layouts.
@ -163,7 +279,7 @@ class DebugLink:
y: Optional[int] = None,
wait: Optional[bool] = None,
hold_ms: Optional[int] = None,
) -> Optional[LayoutLines]:
) -> Optional[LayoutContent]:
if not self.allow_interactions:
return None
@ -176,13 +292,13 @@ class DebugLink:
)
ret = self._call(decision, nowait=not wait)
if ret is not None:
return layout_lines(ret.lines)
return LayoutContent(ret.lines)
return None
def click(
self, click: Tuple[int, int], wait: bool = False
) -> Optional[LayoutLines]:
) -> Optional[LayoutContent]:
x, y = click
return self.input(x=x, y=y, wait=wait)

Loading…
Cancel
Save