mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 15:03:16 +00:00
78 lines
3.6 KiB
Python
Executable File
78 lines
3.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Reads a uiautomator dump of the screen:
|
|
# screen-tree.py <dump .xml> lines the elements with a text or a content description, one per line: "x,y text", the element's centre
|
|
# screen-tree.py <dump .xml> find <text> "x y left top right bottom" of the element with the text, or exits 1
|
|
# screen-tree.py <dump .xml> tree the elements that have a text or can be used: class, text, bounds and states
|
|
# screen-tree.py <dump .xml> focused [<text>] exits 1 unless a text field has the focus, and shows the text exactly
|
|
# Text is compared as the desktop ui helper compares it: letters and digits only, any case. An element that is exactly
|
|
# the text wins over one that contains it, and then one inside a clickable element (Compose puts a button's text in a
|
|
# child of the clickable view) over a title with the same text.
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
BOUNDS = re.compile(r"\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]")
|
|
STATES = ["clickable", "checkable", "checked", "focused", "scrollable", "selected"]
|
|
FIELD = "android.widget.EditText"
|
|
# listed by tree even without a text: fields, and the progress indicators of screens that are busy
|
|
WIDGETS = [FIELD, "android.widget.ProgressBar"]
|
|
|
|
|
|
def normalise(text):
|
|
return "".join(c for c in text.lower() if c.isalnum())
|
|
|
|
|
|
def label(node):
|
|
return " ".join((node.get("text") or node.get("content-desc") or "").split())
|
|
|
|
|
|
# Elements off the screen, such as the chat list while a chat covers it, have empty bounds.
|
|
def nodes(node, in_clickable=False):
|
|
clickable = in_clickable or node.get("clickable") == "true"
|
|
bounds = BOUNDS.fullmatch(node.get("bounds", ""))
|
|
if bounds:
|
|
left, top, right, bottom = map(int, bounds.groups())
|
|
if right > left and bottom > top:
|
|
yield node, clickable, (left, top, right, bottom)
|
|
for child in node:
|
|
yield from nodes(child, clickable)
|
|
|
|
|
|
def tree_line(node, bounds):
|
|
left, top, right, bottom = bounds
|
|
states = [s for s in STATES if node.get(s) == "true"] + (["disabled"] if node.get("enabled") == "false" else [])
|
|
kind = node.get("class", "").rsplit(".", 1)[-1]
|
|
return f"{kind} {json.dumps(label(node))} at {left},{top} size {right - left}x{bottom - top}{''.join(' ' + s for s in states)}"
|
|
|
|
|
|
def main():
|
|
path, command = sys.argv[1], sys.argv[2]
|
|
found = list(nodes(ET.parse(path).getroot()))
|
|
labelled = [(label(n), clickable, bounds) for n, clickable, bounds in found if label(n)]
|
|
if command == "lines":
|
|
for text, _, (left, top, right, bottom) in labelled:
|
|
print(f"{(left + right) // 2},{(top + bottom) // 2} {text}")
|
|
elif command == "tree":
|
|
for node, _, bounds in found:
|
|
if label(node) or node.get("class") in WIDGETS or any(node.get(s) == "true" for s in ["clickable", "checkable", "scrollable"]):
|
|
print(tree_line(node, bounds))
|
|
elif command == "focused":
|
|
text = " ".join(sys.argv[3].split()) if len(sys.argv) > 3 else ""
|
|
if not any(n.get("class") == FIELD and n.get("focused") == "true" and text in label(n) for n, _, _ in found):
|
|
sys.exit(1)
|
|
elif command == "find":
|
|
target = normalise(sys.argv[3])
|
|
matches = [e for e in labelled if target and target in normalise(e[0])]
|
|
if not matches:
|
|
sys.exit(1)
|
|
_, _, (left, top, right, bottom) = min(matches, key=lambda e: (normalise(e[0]) != target, not e[1]))
|
|
print(f"{(left + right) // 2} {(top + bottom) // 2} {left} {top} {right} {bottom}")
|
|
else:
|
|
sys.exit(f"unknown command: {command}")
|
|
|
|
|
|
main()
|