Skip to content

Latest commit

 

History

History
302 lines (268 loc) · 18.7 KB

File metadata and controls

302 lines (268 loc) · 18.7 KB

Capability matrix

Status meanings: stable is compatibility-supported, beta is suitable for evaluation with documented limitations, and experimental may change without a compatibility window.

Capability Status Windows Linux X11 Linux Wayland macOS
Mouse, keyboard, screenshot stable CI CI/Xvfb + xev CI/sway + libeis implementation
JSON executor and variables stable CI CI CI platform-neutral
Image and anchor locators beta CI CI implementation implementation
Accessibility locator beta CI CI/AT-SPI CI/AT-SPI CI (tree read)
Window management beta CI CI/openbox unavailable CI (listing)
Recorder beta CI implementation unavailable CI
Reports, trace, failure bundle stable CI CI CI platform-neutral
REST, MCP, scheduler beta CI CI CI platform-neutral
Remote desktop / WebRTC beta tests tests tests tests
Android and iOS bridges experimental mocked CI mocked CI mocked CI mocked CI
LLM/VLM agents experimental fake-backend CI fake-backend CI fake-backend CI fake-backend CI
USB passthrough experimental hardware-unverified backend tests backend tests hardware-unverified

“Implementation” means code exists but the repository does not currently run a real OS runner for it. It must not be interpreted as a production guarantee. Hardware-backed results and known limitations should be attached to releases.

Linux Wayland is split: capture is exercised by CI against a real compositor; input is exercised by CI against a real EI peer and a real portal.

Linux X11 said CI/Xvfb for a long time on the strength of a job that imported the package under xvfb-run and generated two lines of code. Nothing moved a pointer, and every X11 assertion in the suite is made against a mock of python-Xlib, so the questions that matter went unanswered: does an injected event reach a client at all, does it arrive as real input, and is a captured pixel the pixel on screen. The x11-verification job answers them against a real Xvfb server with a real window manager, and it takes its ground truth from other codebases than the one under test — xev, a real X client that prints every event delivered to its window; ImageMagick's import, an independent grabber, against a root window painted two asymmetric colours so a wrong rectangle cannot look right; and xdotool / xdpyinfo, the server answering for itself. It runs twice, over one monitor and then two.

One assertion there is worth naming, because losing it would be silent: XTest-injected events must arrive with synthetic NO. XSendEvent traffic arrives with synthetic YES and is discarded by most toolkits, so a backend that quietly stopped driving real input would still pass every check that only counted events.

There is deliberately no negative-origin X11 pass. On X11 the root window is the union of every monitor and always begins at (0, 0): a monitor placed to the left shifts the others right rather than moving the origin. The Wayland job's second layout has no analogue here — a protocol difference, not an untested case.

Screen capture runs through the compositor's own tool (grim on wlroots, gnome-screenshot on GNOME, spectacle on KDE), falling back to xdg-desktop-portal over the session bus, instead of the X11-only Pillow/mss path — and JE_AUTOCONTROL_WAYLAND_CAPTURE_COMMAND covers a setup none of those fit. The wayland-verification job in docker/ runs the whole capture path inside a headless sway session and checks it against pixels the compositor painted, which is why this row says CI rather than “implementation”. It runs twice, over two output layouts: side by side from the origin, and with the left-hand output at x=-1280 — the layout of any desktop with a monitor left of the primary one, where the compositor's plane starts at a negative coordinate and a size, a crop or a located hit that assumes (0, 0) is wrong by the width of that monitor.

The recorder row said unavailable for macOS while the code for one sat in the tree unused, and the reason was real rather than an oversight: the old listener built an NSApplication at import time and stopped recording with AppHelper.runEventLoop(), a loop that never returns to its caller. Wiring that up would have put both on the path of import je_auto_control, so wrapper/_platform_osx.py set recorder = None instead.

Neither was necessary. A CGEventTap needs a run loop, not an application: the tap is created on a dedicated thread, its source is added to that thread's run loop, and the loop is pumped in short CFRunLoopRunInMode slices so a stop flag is honoured between them — the same shape the macOS hotkey backend already used. The tap is listen-only, because a recorder that consumed events would swallow the input it is recording. This row says CI because the macos-capabilities job records a real session on a real window server: it posts a move, a click and a keypress through the public API and asserts they come back out of the tap with the release and the coordinates they were posted at.

Two defects were in that code and only a Mac could show them. Coordinates came from NSEvent.mouseLocation(), whose origin is the bottom-left of the display, while every replay posts into the top-left space osx_mouse uses — so a click recorded near the top of the screen replayed near the bottom. And modifiers were not recorded at all: macOS sends no key-down for Shift, Control, Option or Command, only a flagsChanged event carrying the new flag set, so a recording could not say a modifier was held across what followed.

The table has four columns because those are the four desktops with their own backend, not because they are the only supported systems. Two more axes now have CI behind them:

The BSDs. platform_wrapper refused to start on anything that was not win32/cygwin/msys, darwin or linux/linux2, and every X11 backend module carried its own copy of the same Linux-only guard — so a FreeBSD, OpenBSD or NetBSD desktop, which runs the same X server and the same python-Xlib as Linux, could not import the package at all. python-Xlib was pinned to platform_system=='Linux' too, so even relaxing the guards would have left the backend without its one dependency. The guards now ask utils/platform_id.is_x11_unix() — "is this an X11 unix", which is the question they were always trying to ask — and the freebsd job boots a real FreeBSD 14 VM to run that decision on a system that is genuinely one.

For a while it checked that decision and nothing else, for a measured reason: importing anything under je_auto_control ran the package facade, which imported OpenCV and cryptography at module scope, and neither publishes a FreeBSD wheel — installing them from ports pulled a dependency tree that had not finished after fifty minutes.

That was the wrong thing to work around. Moving a pointer needs neither package, so the facade stopped importing them (and NumPy, Pillow and je_open_cv) at module scope; they belong to the functions that use them. What the VM installs now is python-Xlib, defusedxml and an X server, all of which take seconds, and test/verify/freebsd_verify.py drives the whole backend on it. The reads come off the X server rather than out of this codebase: query_pointer for the cursor and the button mask, query_keymap for whether an injected key really went down, and a mapped X window that has asked for button events for the wheel — which is what caught mouse_scroll matching a literal ["linux", "linux2"] and therefore doing nothing at all, silently, on every BSD.

arm64. macos-14 was already arm64; ubuntu-22.04-arm joins the smoke matrix and passes. windows-11-arm was tried and removed, on measurement rather than assumption, and two dependencies are why: opencv-python publishes no win_arm64 wheel in any version, so pip falls back to building from source and CMake cannot configure for ARM64; and cryptography stopped publishing one after 46.0.3, while this project's floor is >=48.0.1 — a security floor (GHSA-537c-gmf6-5ccf) that cannot be lowered to reach a wheel. Neither is a CI problem to work around: the package genuinely cannot be installed on Windows arm64 today. Progress.md records both, alongside a pip --dry-run --platform win_arm64 command that re-checks them in seconds without an arm64 machine.

The accessibility row said backend tests for Linux X11 and meant nothing by it: there was no Linux backend at all, and _build_backend() fell straight through to the null one. There is one now, over AT-SPI2 — which is a D-Bus protocol rather than a library, and that is what makes it reachable without a new dependency. The usual bindings (pyatspi, gi.repository.Atspi) are distribution packages built against the system introspection data and cannot be installed into a virtual environment, so depending on them would be depending on something most users cannot get. The client written for the XDG portal handshake already spoke enough D-Bus.

It is exercised by the x11-verification job against a real accessibility bus and a real GTK application (zenity), because neither half can be mocked usefully: the bus is D-Bus-activated rather than started by hand, an application only appears on it if its toolkit bridge loaded, and the tree's shape is the toolkit's business.

That job immediately found a gap in the shared D-Bus client: it could not demarshal signed integers. The portal handshake never needed one, and AT-SPI reports a component's extents as four signed values — because a window on a monitor left of or above the primary one is at a negative coordinate. Without it the backend could read a tree but not where anything was. The client now handles the whole fixed-width numeric set except h (UNIX_FD), which stays an error on purpose: it is an index into a descriptor array this client does not receive, so returning it would hand a caller a number that addresses nothing.

Because AT-SPI is a bus rather than a display protocol, this row is CI/AT-SPI for both Linux entries: a Wayland session runs the same accessibility bus, so this is the one capability where Wayland is not the restricted case.

Window management had no row here at all until it had more than one platform. It was Windows-only for the project's whole life — the facade branched on sys.platform and raised everywhere else — which left 23 AC_* commands and their MCP tools dead on macOS and Linux. It now goes through a backend seam: Win32, EWMH over python-Xlib on X11, and Quartz plus the accessibility API on macOS.

The X11 half is exercised by the x11-verification job against a real openbox session, driving the public facade and taking ground truth from xwininfo and xprop. Two things only a real window manager could have shown up came out of it, and both were wrong in the first implementation:

  • The rectangle is the frame, not the client. Win32's GetWindowRect returns the frame — border and title bar included — and every caller here is written against that. Reporting the client area was off by the decorations on X11 alone, silently, and by a different amount per window manager.
  • A move must go through _NET_MOVERESIZE_WINDOW. Under a reparenting window manager a client's own x/y are relative to its frame, so a direct ConfigureWindow asks for a position in the wrong coordinate space. Measured against openbox, asking for (300, 220) that way landed the window at (302, 260).

post_key_to_window and post_click_to_window work on X11 and are asserted to arrive flagged synthetic, because that is what they are: XSendEvent traffic, which GTK and Qt discard by design. They are the X11 counterpart of Win32's PostMessage, which carries the same best-effort caveat. macOS has no equivalent at all — an event goes to whatever has focus — so the backend refuses rather than reporting a success that went somewhere else.

Wayland is unavailable and will stay that way: the protocol does not let a client enumerate or move another application's windows. That is a design decision upstream, not a gap here, and the backend selector says so instead of looking broken.

One cross-platform difference falls out of the same job, and it is not one this project can fix: a Wayland capture may contain the mouse cursor. No capture here passes grim -c, so none of them asks for the pointer — but wlroots draws a software cursor whenever the backend has no cursor plane, and a software cursor is composited into the output buffer, which is the buffer wlr-screencopy hands back. Headless is permanently in that state, and so is a real desktop whose driver offers no cursor plane or whose user set WLR_NO_HARDWARE_CURSORS=1, a common workaround. Windows' BitBlt and the X11 Pillow/mss path never include the pointer, so this is a Wayland-only inconsistency rather than something callers already expect: with the pointer resting on its target, a locator, a template match or an OCR read sees a pointer-shaped hole in the middle of it. Both ways out need to know where the pointer is — move it away and back, or mask around it — and Wayland does not let a client read the cursor position, so the only source would be an in-process record that goes stale the moment the user touches their own mouse; masking the wrong place is worse than a visible cursor. So this is documented rather than worked around: park the pointer away from the region of interest before capturing. The seat-verification job asserts the behaviour as measured, so if wlroots ever honours overlay_cursor for software cursors, CI goes red and says so.

Input is verified in four parts, all of which are CI jobs.

The eis-verification job in docker/ runs AutoControl's real libei sender against a real EIS server — libeis, over a Unix socket, with no compositor involved — and reads back off the wire what arrived: the capability and event-type enum values, the variadic seat bind, the key codes, the absolute coordinates, the button codes, the scroll unit and sign, and a frame per emission. It also settles the absolute pointer's coordinate space, which is where the negative-origin layout above reaches the input half: a region's offset is part of the coordinate rather than something to subtract, and a motion landing outside every region is dropped by libei without a return code, an event or an error — so the sender maps the point into region space and refuses what no region covers, which is what lets the ydotool path take it.

The ydotool-verification job covers the CLI fallback, which the libei path drops to at every failure point. A seat is what makes an injected event arrive somewhere; it is not what makes one observable, so no compositor is needed: ydotoold creates an ordinary uinput device, the kernel publishes it as /dev/input/eventN, and the job reads the input_event structs back off it. That covers the click bitmasks, the split press / release edges drag depends on, what mousemove --absolute really puts on the wire, the wheel signs and axes, numeric key codes, and — in the last check — the argv the mouse and keyboard backends build for themselves.

The seat-verification job is where an injected event finally reaches a compositor, and it settles what mousemove --absolute is absolute to. That had been recorded as needing a VM running a desktop that consumes libinput devices; it needs three environment variables instead. wlroots takes WLR_BACKENDS=headless,libinput, so the outputs stay virtual while the input half is the real libinput backend; libseat's builtin backend opens the device without logind; and SEATD_VTBOUND=0 stops it reaching for a VT no container owns. grim -c then draws the cursor into a screenshot, so the compositor answers in layout coordinates. Two findings come out of it, over the same two layouts the capture job uses. The origin --absolute counts from is the top-left of the output layout, not layout (0, 0) — the same distinction the capture path already makes, and the reason set_position now subtracts layout_origin() before calling ydotool. And the displacement is relative motion, so the compositor's pointer acceleration scales it: libinput's default adaptive profile moves the cursor twice as far as asked, which is what ydotool's own --help means by "You need to disable mouse speed acceleration for correct absolute movement". The ydotool fallback is therefore only pixel-accurate on a session whose pointer acceleration is off; the libei path is absolute at the protocol level and is unaffected.

The factor is compositor configuration and no client can read it back, so the library cannot compensate for it — only the operator knows whether it is off. JE_AUTOCONTROL_WAYLAND_POINTER_ACCEL is how they say so, and it applies to the ydotool path alone: unset (or set to anything unrecognised, which says so and falls back) warns once per process and sends the move anyway, flat declares acceleration off and moves silently, and strict refuses the move rather than let a click land somewhere else.

The portal-verification job covers how a client reaches libei on GNOME and KDE, which is not a socket path but a file descriptor handed over D-Bus at the end of the org.freedesktop.portal.RemoteDesktop session dance. That had been recorded as needing a real desktop, on the grounds that no container ships a RemoteDesktop portal — but the portal is a D-Bus interface, so the job owns the well-known name itself and runs the real liboeffis through the real handshake, ending in a live connection to the same libeis server the eis-verification job uses. It settles the call order and the predicted request paths, the device mask a user would be consenting to, that the descriptor carries a real EI session, and that input emitted through it is recorded at the far end. Every refusal is covered too — a dismissed dialog, a dialog left open, a withheld descriptor, a closed session, a portal too old for ConnectToEIS, no portal at all — each of which has to fail closed on AutoControl's own clock.

What is still not covered is the consent dialog as a dialog: no user dismisses anything in CI, so what a real dialog looks like and how long a real one blocks stay mutter's business. The compositor also refuses global input recording, key hooks, cursor-position reads and per-window injection outright; those are Wayland design decisions, not gaps. See Progress.md.

One packaging note that affects users more than any of the above: ydotool 1.0 replaced its entire command line, and everything this backend builds arrived in that release. Debian trixie ships no ydotool package; bookworm and every current Ubuntu ship 0.1.8, which answers this argv with exit code 0 and no events. AutoControl refuses that version up front rather than reporting success for input it never sent.