The WM runs on the HAL direct shell: own cursor, per-output modes with try/confirm/revert, Extended and Mirror (optimise for a screen), pacing setting, a redesigned Display menu with a snapping scale slider, a lower dock whose hits reach the screen edge, clicks only to the topmost window, and asking the AI hub for GPU memory under pressure. Hosted apps state their wire protocol (stale ones are rebuilt), and phone builds list only mobile apps with live home tiles. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
256 lines
8.3 KiB
Python
256 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
"""The user's scenarios, frame by frame (node 165, hidden).
|
|
|
|
scenarios.py <root> <scenario> [--autohide left,right] [--tag T]
|
|
|
|
scenarios: swipe, swipe-fast, shelf, unfold, modes, all
|
|
Writes ~/lanes/wm-review-runs/<tag>/frames.jsonl + marks.json and runs
|
|
analyze.py over it (exit 1 when anything is flagged).
|
|
"""
|
|
import argparse, json, os, re, subprocess as sp, sys, time
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from ftrig import Rig
|
|
|
|
SUPER = dict(logo=1)
|
|
|
|
|
|
def first_frames(rig, n, max_frames=900):
|
|
"""Step until n clients reported a first frame."""
|
|
for _ in range(max_frames // 10):
|
|
txt = open(rig.log, errors="replace").read()
|
|
if len(re.findall(r"client \d+ first frame", txt)) >= n:
|
|
return True
|
|
rig.frames(10, real=0.1)
|
|
return False
|
|
|
|
|
|
def live(rig):
|
|
fr = rig.last()
|
|
return {w["c"]: w for w in fr["w"] if w["cap"] is None}, fr
|
|
|
|
|
|
def by_app(rig, app):
|
|
ws, _ = live(rig)
|
|
for c, w in ws.items():
|
|
if w.get("app") == app:
|
|
return w
|
|
return None
|
|
|
|
|
|
def launch(rig, app, count):
|
|
rig.launch(app)
|
|
ok = first_frames(rig, count)
|
|
rig.frames(40, real=0.03)
|
|
return ok
|
|
|
|
|
|
def caption_point(w):
|
|
s = w["s"]
|
|
return (s[0] + s[2] * 0.5, s[1] + 12)
|
|
|
|
|
|
def build_desks(rig, autohide):
|
|
"""Desk 1: notes + clock, notes on the left shelf. Desk 2: calculator
|
|
maximised + terminal on the right shelf. Desk 3: weather, then
|
|
fullscreen (its own space after desk 3)."""
|
|
rig.mark("setup: desk 1 notes, clock")
|
|
first_frames(rig, 2)
|
|
rig.frames(30, real=0.03)
|
|
n = by_app(rig, "notes")
|
|
if n:
|
|
rig.mouse("click", *caption_point(n))
|
|
rig.frames(5)
|
|
rig.mark("setup: park notes left (SUPER+CTRL+A)")
|
|
rig.key("KeyA", logo=1, ctrl=1)
|
|
rig.frames(50, real=0.03)
|
|
rig.mark("setup: new desk 2 (SUPER+0)")
|
|
rig.key("Key0", **SUPER)
|
|
rig.frames(50, real=0.03)
|
|
launch(rig, "calculator", 3)
|
|
launch(rig, "terminal", 4)
|
|
rig.mark("setup: park terminal right (SUPER+CTRL+D)")
|
|
rig.key("KeyD", logo=1, ctrl=1)
|
|
rig.frames(50, real=0.03)
|
|
c = by_app(rig, "calculator")
|
|
if c:
|
|
rig.mark("setup: maximise calculator (double click caption)")
|
|
x, y = caption_point(c)
|
|
rig.mouse("click", x, y)
|
|
rig.frames(2)
|
|
rig.mouse("click", x, y)
|
|
rig.frames(60, real=0.03)
|
|
rig.mark("setup: new desk 3 (SUPER+0)")
|
|
rig.key("Key0", **SUPER)
|
|
rig.frames(50, real=0.03)
|
|
launch(rig, "weather", 5)
|
|
rig.mark("setup: fullscreen weather (SUPER+F)")
|
|
rig.key("KeyF", **SUPER)
|
|
rig.frames(80, real=0.03)
|
|
rig.mouse("move", 640, 400)
|
|
rig.frames(30, real=0.03)
|
|
|
|
|
|
def swipes(rig, fast):
|
|
# From the fullscreen space at the end: left to desk 3, desk 2, desk 1 and back.
|
|
seq = ["ArrowLeft"] * 3 + ["ArrowRight"] * 3 + ["ArrowLeft"] * 3
|
|
for k in seq:
|
|
rig.mark(f"SUPER+{k}" + (" (fast)" if fast else ""))
|
|
rig.key(k, **SUPER)
|
|
rig.frames(6 if fast else 45, real=0.02)
|
|
rig.frames(60, real=0.03)
|
|
if fast:
|
|
rig.mark("fast back and forth")
|
|
for k in ["ArrowRight", "ArrowLeft", "ArrowRight", "ArrowRight", "ArrowLeft", "ArrowLeft"]:
|
|
rig.key(k, **SUPER)
|
|
rig.frames(3, real=0.02)
|
|
rig.frames(60, real=0.03)
|
|
|
|
|
|
def reveal(rig, w):
|
|
"""An auto-hidden shelf shows when the pointer goes to its screen edge."""
|
|
side = (w.get("pose") or {}).get("side", 0)
|
|
if side == 0:
|
|
return
|
|
fr = rig.last()
|
|
scr = fr["disp"][0]["r"]
|
|
x = scr[0] + 1 if side < 0 else scr[0] + scr[2] - 1
|
|
y = w["s"][1] + w["s"][3] * 0.5
|
|
rig.mark(f"reveal shelf {side}: pointer to the edge")
|
|
rig.mouse("move", x, y)
|
|
rig.frames(40, real=0.03)
|
|
|
|
|
|
def shelf_drag(rig):
|
|
"""Pull notes off the left shelf of desk 1 and drop it back."""
|
|
rig.mark("go to desk 1 (SUPER+1)")
|
|
rig.key("Key1", **SUPER)
|
|
rig.frames(50, real=0.03)
|
|
n = by_app(rig, "notes")
|
|
if not n:
|
|
rig.mark("NO notes window")
|
|
return
|
|
reveal(rig, n)
|
|
n = by_app(rig, "notes")
|
|
s = n["s"]
|
|
x0, y0 = s[0] + s[2] * 0.5, s[1] + s[3] * 0.5
|
|
rig.mark(f"pull notes out of the shelf from {x0:.0f},{y0:.0f}")
|
|
path = [(x0, y0)] + [(x0 + 12 * j, y0 - 2 * j) for j in range(1, 40)]
|
|
rig.drag(path, frames_per_step=1, release=True)
|
|
rig.frames(60, real=0.03)
|
|
n = by_app(rig, "notes")
|
|
s = n["s"]
|
|
x0, y0 = s[0] + s[2] * 0.5, s[1] + 12
|
|
rig.mark("drag notes back to the left edge, dwell, release (edge shelve)")
|
|
path = [(x0, y0)] + [(x0 - (x0 - 1) * j / 30.0, y0) for j in range(1, 31)]
|
|
rig.drag(path, frames_per_step=1, dwell_end=20, release=True)
|
|
rig.frames(80, real=0.03)
|
|
n = by_app(rig, "notes")
|
|
reveal(rig, n)
|
|
n = by_app(rig, "notes")
|
|
s = n["s"]
|
|
rig.mark("flick notes out of the shelf")
|
|
path = [(s[0] + s[2] * 0.5, s[1] + s[3] * 0.5), (s[0] + s[2] * 0.5 + 150, s[1] + s[3] * 0.5), (s[0] + 500, s[1] + s[3] * 0.5)]
|
|
rig.drag(path, frames_per_step=1, release=True)
|
|
rig.frames(80, real=0.03)
|
|
rig.mark("park notes left again (SUPER+CTRL+A)")
|
|
rig.key("KeyA", logo=1, ctrl=1)
|
|
rig.frames(80, real=0.03)
|
|
|
|
|
|
def unfold(rig):
|
|
rig.mark("go to desk 1 (SUPER+1)")
|
|
rig.key("Key1", **SUPER)
|
|
rig.frames(50, real=0.03)
|
|
n = by_app(rig, "notes")
|
|
if not n:
|
|
return
|
|
reveal(rig, n)
|
|
n = by_app(rig, "notes")
|
|
s = n["s"]
|
|
rig.mark("click notes tile: unfold")
|
|
rig.mouse("click", s[0] + s[2] * 0.5, s[1] + s[3] * 0.5)
|
|
rig.frames(60, real=0.03)
|
|
rig.mark("Escape: fold")
|
|
rig.key("Escape")
|
|
rig.frames(60, real=0.03)
|
|
rig.mark("unfold then swipe right while unfolded")
|
|
n = by_app(rig, "notes")
|
|
reveal(rig, n)
|
|
n = by_app(rig, "notes")
|
|
s = n["s"]
|
|
rig.mouse("click", s[0] + s[2] * 0.5, s[1] + s[3] * 0.5)
|
|
rig.frames(30, real=0.03)
|
|
rig.key("ArrowRight", **SUPER)
|
|
rig.frames(50, real=0.03)
|
|
rig.mark("and back")
|
|
rig.key("ArrowLeft", **SUPER)
|
|
rig.frames(60, real=0.03)
|
|
|
|
|
|
def modes(rig):
|
|
rig.mark("go to desk 1 (SUPER+1)")
|
|
rig.key("Key1", **SUPER)
|
|
rig.frames(50, real=0.03)
|
|
c = by_app(rig, "clock")
|
|
if not c:
|
|
return
|
|
x, y = caption_point(c)
|
|
rig.mark("maximise clock (double click)")
|
|
rig.mouse("click", x, y)
|
|
rig.frames(2)
|
|
rig.mouse("click", x, y)
|
|
rig.frames(60, real=0.03)
|
|
rig.mark("restore clock (double click)")
|
|
c = by_app(rig, "clock")
|
|
x, y = caption_point(c)
|
|
rig.mouse("click", x, y)
|
|
rig.frames(2)
|
|
rig.mouse("click", x, y)
|
|
rig.frames(60, real=0.03)
|
|
rig.mark("fullscreen clock (SUPER+F)")
|
|
rig.key("KeyF", **SUPER)
|
|
rig.frames(70, real=0.03)
|
|
rig.mark("leave fullscreen (SUPER+F)")
|
|
rig.key("KeyF", **SUPER)
|
|
rig.frames(70, real=0.03)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("root")
|
|
ap.add_argument("scenario")
|
|
ap.add_argument("--autohide", default="")
|
|
ap.add_argument("--tag")
|
|
ap.add_argument("--realtime", action="store_true", help="no virtual clock: real pacing (analyzed at --hz 60)")
|
|
ap.add_argument("--screens", default="", help="MAKEPAD_WM_FAKE_SCREENS: the per-screen desk of Linux direct, e.g. eDP-1:0,0,1280,800")
|
|
a = ap.parse_args()
|
|
tag = a.tag or f"{a.scenario}-{a.autohide or 'shown'}{'-screens' if a.screens else ''}".replace(",", "+")
|
|
files = {"wm/current-shelf-autohide": a.autohide.replace(",", " ") + "\n"} if a.autohide else {}
|
|
env = {"MAKEPAD_WM_FAKE_SCREENS": a.screens} if a.screens else {}
|
|
if a.realtime:
|
|
tag += "-rt"
|
|
rig = Rig(a.root, tag, apps="notes,clock", files=files, env=env, virtual=not a.realtime).up()
|
|
try:
|
|
build_desks(rig, a.autohide)
|
|
if a.scenario in ("swipe", "all"):
|
|
swipes(rig, False)
|
|
if a.scenario in ("swipe-fast", "all"):
|
|
swipes(rig, True)
|
|
if a.scenario in ("shelf", "all"):
|
|
shelf_drag(rig)
|
|
if a.scenario in ("unfold", "all"):
|
|
unfold(rig)
|
|
if a.scenario in ("modes", "all"):
|
|
modes(rig)
|
|
rig.mark("end")
|
|
rig.frames(5)
|
|
finally:
|
|
rig.down()
|
|
here = os.path.dirname(os.path.abspath(__file__))
|
|
r = sp.run([sys.executable, os.path.join(here, "analyze.py"), rig.trace, "--marks", os.path.join(rig.out, "marks.json")] + (["--hz", "60"] if a.realtime else ["--no-pacing"]))
|
|
sys.exit(r.returncode)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|