#!/usr/bin/env python3 import argparse, json, os, subprocess, time from pathlib import Path def run(*args): return subprocess.check_output(args, text=True).strip() ap = argparse.ArgumentParser() ap.add_argument('action', choices=['snapshot', 'click', 'type', 'key']) ap.add_argument('values', nargs='*') ap.add_argument('--execute', action='store_true') a = ap.parse_args() wid = os.environ['KVM_WINDOW_ID'] if not wid.isdecimal(): raise SystemExit('KVM_WINDOW_ID must be a numeric X11 window ID') if os.environ.get('XDG_SESSION_TYPE') != 'x11': raise SystemExit('This example requires an X11 desktop session') if a.action != 'snapshot' and not a.execute: print(json.dumps({'action': a.action, 'values': a.values, 'dry_run': True})) raise SystemExit(0) run('xdotool', 'windowactivate', '--sync', wid) time.sleep(0.5) if a.action == 'click': if len(a.values) != 2: raise SystemExit('click requires X Y') x, y = map(int, a.values) geo = dict(line.split('=', 1) for line in run('xdotool', 'getwindowgeometry', '--shell', wid).splitlines()) if not (0 <= x < int(geo['WIDTH']) and 0 <= y < int(geo['HEIGHT'])): raise SystemExit('Click is outside the viewer window') run('xdotool', 'mousemove', '--window', wid, str(x), str(y)) run('xdotool', 'click', '1') elif a.action == 'type': if len(a.values) != 1 or len(a.values[0]) > 200: raise SystemExit('type requires one string of at most 200 characters') run('xdotool', 'type', '--clearmodifiers', '--delay', '70', '--', a.values[0]) elif a.action == 'key': if len(a.values) != 1 or a.values[0] not in {'Return', 'Tab', 'Escape'}: raise SystemExit('Allowed keys: Return, Tab, Escape') run('xdotool', 'key', '--clearmodifiers', a.values[0]) elif a.values: raise SystemExit('snapshot takes no values') time.sleep(1) Path('shots').mkdir(exist_ok=True) shot = Path('shots') / f'{time.time_ns()}.png' run('scrot', '--focused', str(shot)) print(json.dumps({'screenshot': str(shot), 'action': a.action}))