Benchmarks / CPU and I/O (per commit) (push) Failing after 6m16s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 42s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 2s
Build and test / Layer separation (push) Successful in 26s
Traceability / Requirement traces (push) Failing after 38s
Build and test / Android (aarch64) (push) Failing after 2m20s
Build and test / Windows (x86_64, cross) (push) Failing after 3m3s
A scene that makes a parent, nests two collections in it by drag and by the menu, files frames into a child and opens the parent to see it count both; stills of the tree and of the menu. The collections recording is re-made now that the bitmap under the cursor is the photograph. drive.py grows a multi-leg drag: a diagonal with much vertical in it is taken by the grid's Flickable as a scroll before the DragArea can claim it, so a drag to the sidebar goes sideways first.
203 lines
5.7 KiB
Python
Executable File
203 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Drive the desktop build from outside, for the manual's screenshots.
|
||
|
||
drive.py launch [args...] start the app on the private X server
|
||
drive.py stop
|
||
drive.py shot OUT.png
|
||
drive.py click X Y [button]
|
||
drive.py move X Y
|
||
drive.py drag X1 Y1 X2 Y2 [steps]
|
||
drive.py type TEXT
|
||
drive.py key KEYSYM...
|
||
drive.py rec OUT.mp4 / cut start and stop a recording of the window
|
||
drive.py where the pointer, in window coordinates
|
||
|
||
Everything is in *window* pixels, at scale 1, with the window at the origin
|
||
of a 1920×1200 Xvfb on `DR_DISPLAY` (`:7`). The app gets its own XDG
|
||
profile under `DR_HOME` (`/var/tmp/dr-manual`), so nothing here touches the
|
||
library or settings of whoever is logged in; `DR_BIN` names the binary.
|
||
|
||
Two things that cost an afternoon, kept here so they cost nobody else one:
|
||
a Slint `TouchArea` wants a *held* press (`mousedown`, a beat, `mouseup`) —
|
||
xdotool's `click` is sometimes dropped; and under XWayland at scale 2 a
|
||
pointer warp lands at twice the coordinate asked for, which is why this runs
|
||
on Xvfb rather than the desktop.
|
||
"""
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
|
||
HOME = os.environ.get('DR_HOME', '/var/tmp/dr-manual')
|
||
DISPLAY = os.environ.get('DR_DISPLAY', ':7')
|
||
BIN = os.environ.get('DR_BIN', 'target/release/darkroom-desktop')
|
||
ENV = dict(
|
||
os.environ,
|
||
XDG_CONFIG_HOME=f'{HOME}/xdg/config',
|
||
XDG_DATA_HOME=f'{HOME}/xdg/data',
|
||
XDG_STATE_HOME=f'{HOME}/xdg/state',
|
||
RUST_LOG='info',
|
||
WINIT_X11_SCALE_FACTOR='1',
|
||
DISPLAY=DISPLAY,
|
||
)
|
||
ENV.pop('WAYLAND_DISPLAY', None)
|
||
os.environ['DISPLAY'] = DISPLAY
|
||
|
||
|
||
def x(*args, check=True):
|
||
return subprocess.run(
|
||
['xdotool', *map(str, args)], capture_output=True, text=True, check=check
|
||
).stdout.strip()
|
||
|
||
|
||
def win():
|
||
return open(f'{HOME}/app.win').read().strip()
|
||
|
||
|
||
def geometry():
|
||
out = x('getwindowgeometry', win())
|
||
pos = out.split('Position: ')[1].split(' ')[0]
|
||
px, py = map(int, pos.split(','))
|
||
return px, py
|
||
|
||
|
||
def launch(args):
|
||
os.makedirs(HOME, exist_ok=True)
|
||
log = open(f'{HOME}/app.log', 'w')
|
||
p = subprocess.Popen([BIN, *args], env=ENV, stdout=log, stderr=subprocess.STDOUT)
|
||
open(f'{HOME}/app.pid', 'w').write(str(p.pid))
|
||
w = ''
|
||
for _ in range(120):
|
||
w = x('search', '--pid', p.pid, '--name', 'DarkRoom', check=False).split('\n')[-1]
|
||
if w:
|
||
break
|
||
time.sleep(0.5)
|
||
time.sleep(1.5)
|
||
W, H = os.environ.get('WIDTH', '1600'), os.environ.get('HEIGHT', '1100')
|
||
x('windowsize', '--sync', w, W, H)
|
||
time.sleep(0.5)
|
||
x('windowmove', '--sync', w, 0, 0)
|
||
x('windowfocus', '--sync', w, check=False)
|
||
open(f'{HOME}/app.win', 'w').write(w)
|
||
print(f'pid {p.pid} win {w}')
|
||
|
||
|
||
def stop():
|
||
try:
|
||
os.kill(int(open(f'{HOME}/app.pid').read()), 15)
|
||
except (OSError, ValueError) as e:
|
||
print(e)
|
||
time.sleep(1)
|
||
|
||
|
||
def shot(out):
|
||
subprocess.run(['import', '-window', win(), out], check=True)
|
||
|
||
|
||
def move(px, py):
|
||
ox, oy = geometry()
|
||
x('mousemove', '--sync', ox + int(px), oy + int(py))
|
||
|
||
|
||
def click(px, py, button=1):
|
||
x('windowfocus', '--sync', win(), check=False)
|
||
move(px, py)
|
||
time.sleep(0.15)
|
||
x('mousedown', button)
|
||
time.sleep(0.12)
|
||
x('mouseup', button)
|
||
|
||
|
||
def drag(x1, y1, x2, y2, steps=20):
|
||
x('windowfocus', '--sync', win(), check=False)
|
||
move(x1, y1)
|
||
time.sleep(0.15)
|
||
x('mousedown', 1)
|
||
time.sleep(0.15)
|
||
for i in range(1, int(steps) + 1):
|
||
t = i / int(steps)
|
||
move(int(x1) + (int(x2) - int(x1)) * t, int(y1) + (int(y2) - int(y1)) * t)
|
||
time.sleep(0.03)
|
||
time.sleep(0.15)
|
||
x('mouseup', 1)
|
||
|
||
|
||
def drag_path(points, steps=20):
|
||
"""A drag through several points: out of the grid sideways first, then
|
||
to the row. A diagonal with much vertical in it is taken by the grid's
|
||
Flickable as a scroll before the DragArea can claim it."""
|
||
x('windowfocus', '--sync', win(), check=False)
|
||
(x1, y1), rest = points[0], points[1:]
|
||
move(x1, y1)
|
||
time.sleep(0.15)
|
||
x('mousedown', 1)
|
||
time.sleep(0.15)
|
||
for x2, y2 in rest:
|
||
for i in range(1, int(steps) + 1):
|
||
t = i / int(steps)
|
||
move(int(x1 + (x2 - x1) * t), int(y1 + (y2 - y1) * t))
|
||
time.sleep(0.03)
|
||
x1, y1 = x2, y2
|
||
time.sleep(0.4)
|
||
x('mouseup', 1)
|
||
|
||
|
||
def rec_start(out):
|
||
size = x('getwindowgeometry', win()).split('Geometry: ')[1].strip()
|
||
ox, oy = geometry()
|
||
p = subprocess.Popen([
|
||
'ffmpeg', '-hide_banner', '-loglevel', 'error', '-f', 'x11grab',
|
||
'-framerate', '15', '-video_size', size, '-i', f'{DISPLAY}+{ox},{oy}',
|
||
'-c:v', 'libx264', '-preset', 'ultrafast', '-qp', '0', '-y', out,
|
||
])
|
||
open(f'{HOME}/rec.pid', 'w').write(str(p.pid))
|
||
time.sleep(0.5)
|
||
|
||
|
||
def rec_stop():
|
||
pid = int(open(f'{HOME}/rec.pid').read())
|
||
os.kill(pid, 2)
|
||
for _ in range(50):
|
||
try:
|
||
os.kill(pid, 0)
|
||
time.sleep(0.1)
|
||
except OSError:
|
||
break
|
||
|
||
|
||
def where():
|
||
out = x('getmouselocation')
|
||
mx, my = [int(v.split(':')[1]) for v in out.split()[:2]]
|
||
ox, oy = geometry()
|
||
print(mx - ox, my - oy)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
cmd, *a = sys.argv[1:]
|
||
if cmd == 'launch':
|
||
launch(a)
|
||
elif cmd == 'stop':
|
||
stop()
|
||
elif cmd == 'shot':
|
||
shot(a[0])
|
||
elif cmd == 'click':
|
||
click(*a)
|
||
elif cmd == 'move':
|
||
move(*a)
|
||
elif cmd == 'drag':
|
||
drag(*a)
|
||
elif cmd == 'type':
|
||
x('windowfocus', '--sync', win(), check=False)
|
||
x('type', '--delay', '120', a[0])
|
||
elif cmd == 'key':
|
||
x('windowfocus', '--sync', win(), check=False)
|
||
x('key', '--delay', '80', *a)
|
||
elif cmd == 'rec':
|
||
rec_start(a[0])
|
||
elif cmd == 'cut':
|
||
rec_stop()
|
||
elif cmd == 'where':
|
||
where()
|
||
else:
|
||
sys.exit(__doc__)
|