Document Windows support and the AutoBuyer components

focus.py now verifies on Windows rather than assuming activation worked. Windows
declines to raise a window for a process that doesn't own the foreground — it
flashes the taskbar and the call returns as if it succeeded — so the runner reads
the foreground window's process back and reports a failure instead of clicking
into a background window. Same gap that was fixed on macOS earlier.

The runner also declares itself DPI-aware at startup. Without it Windows reports
a virtualised screen size and rescales the coordinates it accepts, while the
browser keeps reporting CSS pixels; on a display at 125% or 150% the two disagree
and clicks drift further off the further they are from the top-left.

Browser windows are matched on the owning process rather than the window title, so
an editor with chrome.js open is no longer mistaken for the browser. Linux now says
window management is unsupported there, rather than reporting no browser found —
pygetwindow has no X11 backend, and "no browser window" reads like Chrome is shut.

The main README gained a section on the AutoBuyer: what the three pieces are, how
to load the extension, and that neither the extension nor the runner reloads
itself when the source changes. That last point has been the cause of most of the
confusing failures so far, so it is called out in Updating too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-30 13:36:47 -05:00
co-authored by Claude Opus 5
parent 65a1103cda
commit 37adbf67fc
6 changed files with 277 additions and 18 deletions
+72 -7
View File
@@ -43,9 +43,62 @@ would interleave clicks.
pip install -r requirements.txt
```
On **macOS** you must grant Accessibility permission to whatever runs the script
(Terminal, iTerm, VS Code) — System Settings → Privacy & Security → Accessibility.
Without it `pyautogui` moves nothing and fails silently.
The macOS-only backends are gated behind platform markers, so this installs the
right set on either OS.
### macOS
Grant Accessibility permission to whatever runs the script — Terminal, iTerm,
VS Code — under System Settings → Privacy & Security → Accessibility. Without it
`pyautogui` moves nothing and fails silently, which looks identical to a bad
selector.
### Windows
No extra permissions, but two things differ.
**Display scaling.** The runner declares itself DPI-aware at startup, and prints
which mode it got (`per-monitor DPI aware`). That stops Windows reporting a
virtualised screen size and rescaling the coordinates it accepts, which would
otherwise put clicks progressively further off as you move from the top-left.
**Foreground lock.** Windows refuses to raise a window for a process that doesn't
already own the foreground — it flashes the taskbar instead, and the activation
call returns as if it worked. The runner verifies by reading the foreground
window's process, so a failure is reported rather than clicked into. If runs stop
with *"the browser is not frontmost"*, click the Chrome window once by hand and
try again; something else is holding the foreground.
Browser windows are matched on the owning process (`chrome.exe`, `msedge.exe`,
`brave.exe`), not the window title, so an editor with `chrome.js` open won't be
mistaken for the browser.
### First run on a new machine
Verify in this order — each step is harmless on its own, and the first one that
looks wrong tells you where the problem is:
```bash
# 1. Can the extension see the page, and do the coordinates look sane?
python clicker.py locate "a.add_account_btn"
# 2. Does the cursor actually land on the element? Nothing is pressed.
python clicker.py click "a.add_account_btn" --dry-run
# 3. A real click.
python clicker.py click "a.add_account_btn"
```
At step 1, check the reported desktop x,y against where the element actually is
on screen. A consistent offset means display scaling was misdetected — pass
`--scale` explicitly. Only then start the daemon.
### Linux / X11
Not supported. `pygetwindow`, which raises the browser window, has no X11 backend,
so every click is refused with *"window management is unsupported on linux"*.
Making it work needs an `xdotool` or `wmctrl` path in `focus.py`, a real window
manager (Xvfb alone has no focus semantics), and X11 rather than Wayland.
## Use
@@ -74,10 +127,11 @@ python clicker.py click ".trade-btn" --index 2 --url "https://tradeify.co/*"
| Flag | Meaning |
|---|---|
| `--url` | Chrome match pattern for the tab. Without it, the active tab is used. |
| `--url` | Chrome match pattern for the tab. Without it, the hosts in the extension manifest. |
| `--open-url` | Page to open if no tab matches `--url`. |
| `--index` | Which match, when the selector hits several (default 0). |
| `--api` | Dashboard URL (default `http://localhost:3000`). |
| `--timeout` | Seconds to wait for the extension (default 20). |
| `--timeout` | Seconds to wait for the extension (default 45 — a cold page load takes time). |
| `--scale` | CSS-to-desktop pixel ratio. Auto-detected; override if clicks land off. |
| `--dry-run` | Move the cursor, don't press. |
| `--force` | Click even when something is covering the element. |
@@ -144,6 +198,11 @@ On macOS this uses `NSWorkspace` via pyobjc, which needs no Automation permissio
activating an app is not scripting it. Without pyobjc it falls back to `osascript`,
which does prompt for Automation permission the first time.
On Windows it goes through `pygetwindow`, then confirms by reading the foreground
window's process. That confirmation is the important half: Windows silently
declines to raise a window for a background process, and the activation call
reports success either way.
## Cursor motion
`humanize.py` moves the pointer the way a hand does rather than teleporting:
@@ -187,7 +246,13 @@ Exit codes: `0` ok, `1` error, `2` capture switch off, `3` refused to click
relative to the primary display's origin. A Chrome window on a secondary monitor
usually still works, but verify with `locate` before trusting `click`.
- **Display scaling**: the scale factor is inferred by comparing the screen width
the OS reports against the one the browser reports. On macOS and unscaled Windows
this is 1:1. If clicks land at a consistent offset, set `--scale` explicitly.
the OS reports against the one the browser reports, and is only trusted when it
lands on a real scaling factor (1.0, 1.25, 1.5, …). Anything else falls back to
1:1 with a warning, because on a multi-monitor desktop the two sides describe
different displays and the ratio is meaningless. If clicks land at a consistent
offset, set `--scale` explicitly.
- **Focus is exclusive**: the browser must be frontmost at the moment of each
click, so the machine can't be used for anything else during a run, and a stray
dialog stealing focus fails the step.
- The measurement and the click are separate moments. If the page moves the element
in between (a re-render, a late-loading banner), the click lands where it *was*.