diff options
Diffstat (limited to 'lib')
| -rwxr-xr-x | lib/uinodes.py | 88 |
1 files changed, 88 insertions, 0 deletions
diff --git a/lib/uinodes.py b/lib/uinodes.py new file mode 100755 index 0000000..5e87a3f --- /dev/null +++ b/lib/uinodes.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Parse a uiautomator dump into labelled, tappable nodes. + +Kept in its own file rather than inlined into the shell script: the first version was a +python -c embedded in a bash case block, and the nested quoting silently mangled the regexes +so the dump reported one element and a tap landed on the wrong one. + +Usage: + uinodes.py list < dump.xml + uinodes.py find <label> < dump.xml +""" +import re +import sys + +NODE = re.compile(r"<node\b[^>]*>") +DESC = re.compile(r'content-desc="([^"]*)"') +TEXT = re.compile(r'text="([^"]*)"') +BOUNDS = re.compile(r'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') +CLICKABLE = re.compile(r'clickable="true"') + + +def nodes(raw): + for match in NODE.findall(raw): + bounds = BOUNDS.search(match) + if not bounds: + continue + x1, y1, x2, y2 = map(int, bounds.groups()) + if x2 <= x1 or y2 <= y1: + continue + desc = (DESC.search(match) or [None, ""])[1] if DESC.search(match) else "" + text = (TEXT.search(match) or [None, ""])[1] if TEXT.search(match) else "" + label = desc.strip() or text.strip() + if not label: + continue + yield { + "label": label, + "kind": "desc" if desc.strip() else "text", + "x": (x1 + x2) // 2, + "y": (y1 + y2) // 2, + "clickable": bool(CLICKABLE.search(match)), + } + + +def main(): + if len(sys.argv) < 2: + print("usage: uinodes.py list|find <label>", file=sys.stderr) + return 2 + raw = sys.stdin.read() + found = list(nodes(raw)) + + if sys.argv[1] == "list": + if not found: + print(" (no labelled nodes found -- is anything on screen?)") + seen = set() + for node in found: + key = (node["label"], node["x"], node["y"]) + if key in seen: + continue + seen.add(key) + tap = "tappable" if node["clickable"] else " " + print(f" [{node['kind']:4}] {tap} {node['label']!r} @ ({node['x']},{node['y']})") + return 0 + + if sys.argv[1] == "find": + target = sys.argv[2] + hits = [n for n in found if n["label"] == target] + # Prefer a clickable node: a label and the button carrying it are often separate nodes + # at different coordinates, and tapping the label does nothing. + clickable = [n for n in hits if n["clickable"]] + if clickable: + hits = clickable + # Collapse nodes that resolve to the same point. + unique = {(n["x"], n["y"]) for n in hits} + if not unique: + print("NONE") + elif len(unique) > 1: + print(f"AMBIGUOUS {len(unique)}") + else: + x, y = unique.pop() + print(f"{x} {y}") + return 0 + + print(f"unknown command {sys.argv[1]}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) |
