summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rwxr-xr-xlib/uinodes.py88
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())