#!/usr/bin/env python3 """Turns `iris::input` debug lines -- from a phone's diagnostics report, or from a report the layer-1 harness produced with tracing on (`iris::diagnostics::set_trace(true)`) -- back into a `TouchScript` file `iris::harness::Harness::replay` can play back at layer 1. Why this exists: `docs/RUST.md`'s "Three test layers" box says the cheapest layer that can answer a question wins, and a gesture that misbehaves on Iris's phone is otherwise only describable in words. `iris::sense:: log_input_event`'s one line per platform event (Android's on_touch_event once per `MotionEvent`, with historical samples inline; winit's once per pointer `WindowEvent`; the harness's `touch`, once per script line) already carries everything a `.touch` file's `t_ms action x y` needs -- this just reads it back out and reconstructs the samples in order, expanding each event's inline historical samples into their own `move` lines first (they are always intermediate positions of a move, and Android documents them as oldest first, which is also the order they appear in the line). Usage: report_to_touch.py < report.txt > replay.touch report_to_touch.py report.txt > replay.touch Only lines containing "iris input: action=..." are read; everything else in the report (insets, frame timings, drag-release summaries) is ignored, so this can be pointed at Copy report's whole clipboard text directly. """ import re import sys # The message half of `sense::log_input_event`'s format string, prefix- # agnostic: a real report line also carries the ring's own # `HH:MM:SS.mmm LEVEL target:` header (`LogLine::format`) or, forwarded # through `ai_server::client_log`, a `[ #]` tag ahead # of that -- neither of which this needs to understand, since `search` # (not `match`) finds the marker wherever it starts. LINE_RE = re.compile( r"iris input: action=(?P\w+) x=(?P-?[0-9.]+) y=(?P-?[0-9.]+) " r"t=(?P[0-9]+)ms history=(?P[0-9]+)(?P.*)$" ) HIST_RE = re.compile(r"(?P[0-9]+):(?P-?[0-9.]+),(?P-?[0-9.]+)") def _fmt(value: float) -> str: """The number as `TouchScript::parse`'s own `f32::parse` would round-trip it -- an integer without a trailing `.0` where the source was one (every coordinate here is a physical pixel), `{:g}` otherwise so a fractional value from a real device is not silently truncated.""" if value == int(value): return str(int(value)) return f"{value:g}" def convert(lines): """Every `iris::input` line, oldest first, expanded to one `(t_ms, action, x, y)` tuple per touch sample -- a historical sample is always an intermediate `move`, and the event's own sample keeps its real action (`down`/`move`/`up`/`cancel`).""" rows = [] for line in lines: m = LINE_RE.search(line) if not m: continue hist_count = int(m.group("hist")) hist_matches = list(HIST_RE.finditer(m.group("rest"))) if len(hist_matches) != hist_count: print( f"report_to_touch: {line.strip()!r} says history={hist_count} but " f"holds {len(hist_matches)} samples -- skipped", file=sys.stderr, ) continue for hm in hist_matches: rows.append( (int(hm.group("t")), "move", float(hm.group("x")), float(hm.group("y"))) ) rows.append( (int(m.group("t")), m.group("action"), float(m.group("x")), float(m.group("y"))) ) return rows def main(): if len(sys.argv) > 2: print("usage: report_to_touch.py [report.txt] < report.txt", file=sys.stderr) return 2 text = open(sys.argv[1]) if len(sys.argv) == 2 else sys.stdin for t_ms, action, x, y in convert(text): print(f"{t_ms} {action} {_fmt(x)} {_fmt(y)}") return 0 if __name__ == "__main__": sys.exit(main())