summaryrefslogtreecommitdiff
path: root/lib/uinodes.py
blob: 5e87a3fa93a61a18537c82b64a82ffdee4fd93a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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())