Add autobuyer page capture, browser extension, and desktop clicker

Builds the pipeline the autobuyer needs: see the page, find an element,
click it.

extension/ — MV3 Chromium extension. Polls /api/autobuyer/status and,
while on, scrapes the target tab's HTML and posts it back. Also serves
locate requests: focuses the window, scrolls the element into view, and
reports its position. host_permissions is scoped to tradeify plus
localhost so it cannot read other sites — an empty target pattern would
otherwise capture whatever tab happened to be active, including banking
or mail.

app/api/autobuyer/ — status toggle, capture store, and the locate request
queue. CORS is open because the extension's origin changes every time an
unpacked extension is reloaded.

app/autobuyer/page.tsx — ON switch, source view (default) and a rendered
view. The render uses sandbox="allow-scripts" without allow-same-origin:
the page's own JS is needed because sites ship content at opacity:0 and
fade it in, but the frame must not reach the dashboard's same-origin API
routes, which serve firm credentials.

clicker/ — Python CLI. Asks the extension where a selector is, adds the
element rect to the window's screen position and the browser chrome
height to get desktop coordinates, then clicks with a human motion model
(curved path, eased velocity, occasional overshoot, dwell before press).
Raises the browser application first, since macOS consumes a click on an
unfocused window rather than delivering it.

Refuses to click when the element is covered by an overlay, when the
coordinates fall off-screen, or when the browser cannot be confirmed
frontmost.

Verified: API round-trips, capture pruning, locate claim-once semantics,
motion geometry and timing, and focus activation — the last two against
stubs, since pyautogui and pyobjc are not installed here. NOT verified
end to end: Chrome is still running a stale build of the extension, so a
locate request has never completed against a real page and no real click
has been sent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-27 16:15:18 -05:00
co-authored by Claude Opus 5
parent 24a54c6642
commit 54221bbc0c
19 changed files with 1752 additions and 2 deletions
+126
View File
@@ -0,0 +1,126 @@
# Clicker
Moves the real desktop mouse and clicks an element you name with a CSS selector.
The extension can see the DOM but can't move the mouse; this script can move the
mouse but can't see inside Chrome. They meet at the dashboard API:
```
clicker --POST /api/autobuyer/locate------> "where is button.buy?"
extension --POST /api/autobuyer/locate/claim takes the request
extension raises the Chrome window, brings
the tab to the front, scrolls the
element to centre, measures it
extension --POST /api/autobuyer/locate/result desktop x,y
clicker --GET /api/autobuyer/locate?id=---> reads the answer
clicker moves the mouse, clicks
```
## Setup
```bash
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.
## Use
The AutoBuyer switch on the dashboard is the master arm: the script refuses to run
while it's off.
```bash
# Measure only — no mouse movement. Start here.
python clicker.py locate "button.buy"
# Move the cursor to the target but don't press.
python clicker.py click "button.buy" --dry-run
# Actually click.
python clicker.py click "button.buy"
# Pin it to a specific tab, and pick the 3rd match.
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. |
| `--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). |
| `--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. |
| `--robotic` | Straight-line move and instant click, skipping the motion model. |
| `--seed` | Seed the motion RNG so a run replays identically (debugging). |
| `--no-activate` | Don't raise the browser first. The click may then be swallowed. |
## Window focus
A click on a window that isn't focused is consumed by the window manager
*activating* that window — it never reaches the control underneath. That's why an
automated click against a background Chrome appears to do nothing the first time
and work the second: the first click only brought Chrome forward.
The extension calls `chrome.windows.update({focused: true})`, but that only orders
windows **within** Chrome. If the frontmost *application* is your terminal — which
it is, since that's where you launched this — Chrome is still in the background.
So `focus.py` raises the browser application itself immediately before the press,
then confirms it actually came forward before committing to the click. If it can't
verify, it refuses rather than firing a click that would be eaten (exit code 3).
On macOS this uses `NSWorkspace` via pyobjc, which needs no Automation permission —
activating an app is not scripting it. Without pyobjc it falls back to `osascript`,
which does prompt for Automation permission the first time.
## Cursor motion
`humanize.py` moves the pointer the way a hand does rather than teleporting:
- a curved (cubic Bézier) path instead of a straight line, bowing to one side
- eased velocity — accelerate out, coast, decelerate in
- sub-pixel tremor that decays near the target, so the landing stays exact
- long throws (>260px) sometimes overshoot slightly and pull back
- a 60170ms dwell after arriving, before the press
- the button held down 55120ms rather than an instant down/up
This is about reliability as much as appearance. Plenty of web controls only arm
once they have actually been hovered — dropdowns, tooltip-gated buttons, custom
widgets — and a cursor that arrives and presses in the same tick can outrun the
page's own `mousemove` handlers. The dwell is what lets those catch up.
Note `pyautogui.PAUSE` is set to 0 on import: it otherwise sleeps 0.1s after
*every* call, which would add tens of seconds across a stepped path.
Exit codes: `0` ok, `1` error, `2` capture switch off, `3` refused to click
(covered element, or coordinates off-screen).
## Safety
- **Failsafe**: slam the cursor into a screen corner to abort mid-run.
- **Covered elements**: before reporting, the extension checks
`document.elementFromPoint()` at the target centre. If a cookie banner or modal
is on top, the click is refused rather than sent into the overlay — `--force`
overrides.
- **Off-screen check**: coordinates outside the display bounds are refused, which
catches a Chrome window on a second monitor or partly off the edge.
- The element is scrolled to the centre of the viewport before measuring, so a
target below the fold is handled rather than mis-clicked.
## Known limits
- **Latency** is bounded by the extension's poll interval (default 3s), so a click
takes a few seconds to fire. Drop the interval in the extension popup if that
matters.
- **Multi-monitor**: coordinates come from `window.screenX/screenY`, which are
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 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*.
+223
View File
@@ -0,0 +1,223 @@
#!/usr/bin/env python3
"""Desktop mouse control for the AutoFirmer autobuyer.
The browser extension can see the DOM but can't move the mouse; this script can
move the mouse but can't see inside Chrome. They meet at the dashboard API:
clicker --POST /api/autobuyer/locate--> "where is button.buy?"
extension raises the window, scrolls the
element into view, measures it
clicker --GET /api/autobuyer/locate--> desktop x,y
clicker moves the real mouse and clicks
Usage:
python clicker.py locate "button.buy" # measure only, no clicking
python clicker.py click "button.buy" # measure, then really click
python clicker.py click ".btn" --index 2 --url "https://tradeify.co/*"
python clicker.py click "button.buy" --dry-run # move the cursor, don't press
Requires the AutoBuyer page switch to be ON — that's the master arming switch.
"""
import argparse
import json
import random
import sys
import time
import urllib.error
import urllib.request
import focus
DEFAULT_API = "http://localhost:3000"
POLL_INTERVAL = 0.25
class DashboardError(RuntimeError):
pass
class Dashboard:
"""Thin client for the autobuyer endpoints."""
def __init__(self, base: str, timeout: float = 10.0):
self.base = base.rstrip("/")
self.timeout = timeout
def _request(self, path: str, method: str = "GET", payload: dict | None = None) -> dict:
data = json.dumps(payload).encode() if payload is not None else None
headers = {"Content-Type": "application/json"} if data else {}
req = urllib.request.Request(self.base + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as exc:
body = exc.read().decode(errors="replace")
try:
message = json.loads(body).get("error", body)
except json.JSONDecodeError:
message = body
raise DashboardError(f"{method} {path} -> HTTP {exc.code}: {message}") from None
except urllib.error.URLError as exc:
raise DashboardError(f"Cannot reach {self.base}{exc.reason}") from None
def status(self) -> dict:
return self._request("/api/autobuyer/status")
def locate(self, selector: str, index: int, url_pattern: str, timeout: float) -> dict:
"""Queue a lookup and block until the extension answers it."""
queued = self._request(
"/api/autobuyer/locate",
"POST",
{"selector": selector, "index": index, "urlPattern": url_pattern},
)
request_id = queued["id"]
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
row = self._request(f"/api/autobuyer/locate?id={request_id}")
if row["status"] == "done":
return row["result"]
if row["status"] == "error":
raise DashboardError(f"Extension could not locate it: {row['error']}")
time.sleep(POLL_INTERVAL)
raise DashboardError(
f"No answer within {timeout:g}s. Is the extension installed, is Chrome "
f"running, and is the AutoBuyer switch ON?"
)
def scale_factor(found: dict, override: float | None) -> float:
"""CSS pixels and desktop pixels are the same on macOS and on unscaled Windows,
but Windows display scaling and some Linux setups break that. Compare the
screen size the browser reports against the one the OS reports."""
if override is not None:
return override
try:
import pyautogui
except ImportError:
return 1.0 # `locate` is useful without the mouse library installed
os_width = pyautogui.size().width
css_width = (found.get("screenSize") or {}).get("width")
if not css_width:
return 1.0
ratio = os_width / css_width
# Only trust a clean-ish ratio; anything odd means a multi-monitor layout we
# shouldn't guess at, so fall back to 1:1 and let --scale override.
return ratio if 0.4 < ratio < 4.0 else 1.0
def describe(found: dict, factor: float) -> str:
x, y = found["screen"]["x"] * factor, found["screen"]["y"] * factor
lines = [
f" matched <{found['tag']}> {found['text']!r}"
+ (f" (1 of {found['matchCount']})" if found["matchCount"] > 1 else ""),
f" page {found['url']}",
f" viewport x={found['viewport']['x']:.0f} y={found['viewport']['y']:.0f}"
f" {found['viewport']['width']:.0f}x{found['viewport']['height']:.0f}",
f" desktop x={x:.0f} y={y:.0f}" + (f" (scale {factor:g})" if factor != 1 else ""),
]
if found.get("covered"):
lines.append(f" WARNING something is on top of it: {found['coveredBy']}")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("action", choices=["locate", "click"],
help="locate = measure only; click = measure then click")
parser.add_argument("selector", help="CSS selector of the target element")
parser.add_argument("--index", type=int, default=0, help="which match, if the selector hits several (default 0)")
parser.add_argument("--url", default="", help='Chrome match pattern for the tab, e.g. "https://tradeify.co/*"')
parser.add_argument("--api", default=DEFAULT_API, help=f"dashboard URL (default {DEFAULT_API})")
parser.add_argument("--timeout", type=float, default=20.0, help="seconds to wait for the extension (default 20)")
parser.add_argument("--scale", type=float, default=None, help="CSS-to-desktop pixel ratio (default: auto-detect)")
parser.add_argument("--dry-run", action="store_true", help="move the cursor to the target but do not press")
parser.add_argument("--force", action="store_true", help="click even if something is covering the element")
parser.add_argument("--robotic", action="store_true",
help="straight-line move and instant click, skipping the human motion model")
parser.add_argument("--seed", type=int, default=None,
help="seed the motion RNG so a run is reproducible (for debugging)")
parser.add_argument("--no-activate", action="store_true",
help="do not raise the browser first (the click may be eaten by window activation)")
args = parser.parse_args()
dash = Dashboard(args.api)
try:
if not dash.status().get("enabled"):
print("AutoBuyer capture is OFF — turn it on from the dashboard first.", file=sys.stderr)
return 2
print(f"Locating {args.selector!r}", flush=True)
found = dash.locate(args.selector, args.index, args.url, args.timeout)
except DashboardError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
if args.action == "locate":
print(describe(found, scale_factor(found, args.scale)))
return 0
if found.get("covered") and not args.force:
print(describe(found, 1.0))
print("\nRefusing to click: the element is covered — the click would hit "
f"{found['coveredBy']} instead. Pass --force to click anyway.", file=sys.stderr)
return 3
try:
import pyautogui
except ImportError:
print("error: pyautogui is not installed — run: pip install -r requirements.txt", file=sys.stderr)
return 1
# Slamming the cursor into a screen corner aborts the script.
pyautogui.FAILSAFE = True
factor = scale_factor(found, args.scale)
x = found["screen"]["x"] * factor
y = found["screen"]["y"] * factor
screen_w, screen_h = pyautogui.size()
if not (0 <= x < screen_w and 0 <= y < screen_h):
print(describe(found, factor))
print(f"\nerror: target ({x:.0f}, {y:.0f}) is off-screen ({screen_w}x{screen_h}). "
f"Is the Chrome window partly off the display, or on a second monitor?", file=sys.stderr)
return 3
print(describe(found, factor))
# A click on a background window is eaten by the window manager activating it,
# so the first attempt silently does nothing. Raise the browser first, and do it
# here rather than before locating: the measurement takes seconds, and focus is
# only required at the moment of the press.
if not args.no_activate:
focused = focus.ensure_frontmost()
print(f" focus {focused.detail}")
if not focused.ok:
print("\nRefusing to click: the browser is not frontmost, so the click "
"would be consumed activating its window instead of pressing the "
"element. Pass --no-activate to override.", file=sys.stderr)
return 3
if args.robotic:
pyautogui.moveTo(x, y, duration=0.25)
if not args.dry_run:
pyautogui.click()
else:
import humanize
rng = random.Random(args.seed) if args.seed is not None else random.Random()
humanize.click(x, y, rng=rng, press=not args.dry_run)
if args.dry_run:
print("\ndry run — cursor moved, no click sent.")
else:
print("\nclicked.")
return 0
if __name__ == "__main__":
sys.exit(main())
+140
View File
@@ -0,0 +1,140 @@
"""Bring the browser to the front before clicking.
macOS (and Windows, to a lesser degree) treats a click on an unfocused window as
an activation gesture: the click raises the window and is swallowed there, never
reaching the control underneath. So an automated click against a background Chrome
does nothing at all the first time, then works on the second attempt — which looks
like a flaky clicker but is really the window manager doing its job.
The extension already calls chrome.windows.update({focused: true}), but that only
orders windows *within* Chrome. If the frontmost application is your terminal —
which it is, because that's where this script was launched — Chrome as a whole is
still in the background. This module raises the application itself.
"""
import subprocess
import sys
import time
# Chrome ships under several bundle ids; accept whichever is installed.
MAC_BUNDLES = (
"com.google.Chrome",
"com.google.Chrome.beta",
"com.google.Chrome.dev",
"com.google.Chrome.canary",
"com.brave.Browser",
"com.microsoft.edgemac",
)
SETTLE = 0.20 # let the window manager finish raising before measuring or clicking
class FocusResult:
def __init__(self, ok: bool, detail: str):
self.ok = ok
self.detail = detail
def __bool__(self) -> bool:
return self.ok
def _mac_workspace():
"""NSWorkspace via pyobjc. Unlike AppleScript this needs no Automation
permission — activating an app is not scripting it."""
try:
from AppKit import NSWorkspace
except ImportError:
return None
return NSWorkspace.sharedWorkspace()
def frontmost() -> str | None:
"""Bundle id (macOS) or process name of the frontmost application."""
if sys.platform == "darwin":
ws = _mac_workspace()
if ws is None:
return None
app = ws.frontmostApplication()
return app.bundleIdentifier() if app else None
return None
def _mac_activate() -> FocusResult:
ws = _mac_workspace()
if ws is None:
# pyobjc's AppKit isn't present. osascript works but may prompt for
# Automation permission the first time.
try:
subprocess.run(
["osascript", "-e", 'tell application "Google Chrome" to activate'],
check=True, capture_output=True, timeout=5,
)
return FocusResult(True, "activated via osascript")
except Exception as exc:
return FocusResult(False, f"could not activate Chrome ({exc})")
running = {a.bundleIdentifier(): a for a in ws.runningApplications()}
for bundle in MAC_BUNDLES:
app = running.get(bundle)
if app is None:
continue
# NSApplicationActivateIgnoringOtherApps — take focus even though the
# terminal currently owns it.
app.activateWithOptions_(1 << 1)
return FocusResult(True, f"activated {bundle}")
return FocusResult(False, "no Chrome-family browser is running")
def _other_activate() -> FocusResult:
"""Windows/Linux: pyautogui already depends on pygetwindow, so use it."""
try:
import pygetwindow
except ImportError:
return FocusResult(False, "pygetwindow unavailable — cannot raise the browser")
wins = [w for w in pygetwindow.getAllWindows()
if w.title and "Chrome" in w.title and w.visible]
if not wins:
return FocusResult(False, "no Chrome window found")
try:
win = wins[0]
if getattr(win, "isMinimized", False):
win.restore()
win.activate()
return FocusResult(True, f"activated window {win.title[:40]!r}")
except Exception as exc:
return FocusResult(False, f"could not activate window ({exc})")
def activate_browser() -> FocusResult:
"""Raise the browser application above everything else."""
result = _mac_activate() if sys.platform == "darwin" else _other_activate()
if result.ok:
time.sleep(SETTLE)
return result
def ensure_frontmost(timeout: float = 1.5) -> FocusResult:
"""Raise the browser and, where we can check, confirm it actually came forward.
Returning ok=False does not mean the click will fail — only that we could not
verify. The caller decides whether to proceed.
"""
result = activate_browser()
if not result.ok:
return result
if sys.platform != "darwin":
return result # no cheap way to verify; activation call succeeded
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
front = frontmost()
if front is None:
return FocusResult(True, result.detail + " (unverified)")
if front in MAC_BUNDLES:
return FocusResult(True, f"{front} is frontmost")
time.sleep(0.05)
return FocusResult(False, f"browser did not come to the front (frontmost is {frontmost()})")
+135
View File
@@ -0,0 +1,135 @@
"""Human-like cursor motion for pyautogui.
A straight-line teleport followed by an instant click is not just conspicuous — it
is unreliable. Plenty of web UIs only arm a control once it has actually been
hovered (dropdowns, custom widgets, tooltip-gated buttons), and a cursor that
arrives and presses in the same tick can beat the page's own mousemove handlers.
So the motion here does what a hand does: accelerates out, coasts, decelerates in
along a slightly curved path, occasionally overshoots and corrects, pauses a beat
before pressing, and holds the button down for a human interval.
"""
import math
import random
import time
import pyautogui
# pyautogui sleeps PAUSE seconds after *every* call. With a stepped path that
# would add tens of seconds, so we take over timing entirely.
pyautogui.PAUSE = 0
# Motion feel. Durations in seconds, distances in pixels.
MIN_DURATION = 0.15
MAX_DURATION = 1.70
CURVE_STRENGTH = 0.18 # lateral bow, as a fraction of travel distance
TREMOR = 0.7 # sub-pixel hand tremor
OVERSHOOT_ABOVE = 260.0 # only long throws overshoot
OVERSHOOT_CHANCE = 0.55
DWELL = (0.06, 0.17) # settle after arriving, before pressing
HOLD = (0.055, 0.12) # how long the button stays down
def _ease(t: float) -> float:
"""Smootherstep: zero velocity at both ends, quick through the middle."""
return t * t * t * (t * (t * 6 - 15) + 10)
def _bezier(p0, p1, p2, p3, t):
"""Cubic Bezier — the bow that keeps the path off a dead-straight line."""
u = 1 - t
return (
u * u * u * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t * t * t * p3[0],
u * u * u * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t * t * t * p3[1],
)
def _duration_for(distance: float, rng: random.Random) -> float:
"""Farther costs more, but sub-linearly — pointing time grows roughly with the
square root of distance over the range a screen covers. Calibrated so a nudge
of ~40px takes ~0.2s and a throw across a large display takes ~0.8s; a log
curve here would spend a full second creeping 40 pixels."""
base = 0.10 + 0.018 * math.sqrt(distance)
return max(MIN_DURATION, min(MAX_DURATION, base * rng.uniform(0.85, 1.2)))
def _glide(start, end, duration: float, rng: random.Random) -> None:
"""One curved, eased sweep from start to end."""
dx, dy = end[0] - start[0], end[1] - start[1]
distance = math.hypot(dx, dy)
if distance < 1:
return
# Control points pushed perpendicular to the direction of travel, so the path
# bows to one side the way an arm swings rather than tracking a ruler.
nx, ny = -dy / distance, dx / distance
bow = distance * CURVE_STRENGTH * rng.uniform(-1, 1)
c1 = (start[0] + dx * 0.3 + nx * bow, start[1] + dy * 0.3 + ny * bow)
c2 = (start[0] + dx * 0.7 + nx * bow * rng.uniform(0.4, 1.0),
start[1] + dy * 0.7 + ny * bow * rng.uniform(0.4, 1.0))
steps = max(14, min(95, int(distance / 5)))
step_time = duration / steps
next_at = time.perf_counter()
for i in range(1, steps + 1):
t = _ease(i / steps)
x, y = _bezier(start, c1, c2, end, t)
# Tremor fades out as we close in, so the landing stays accurate.
if i < steps:
decay = 1 - (i / steps)
x += rng.gauss(0, TREMOR) * decay
y += rng.gauss(0, TREMOR) * decay
pyautogui.moveTo(x, y, duration=0, _pause=False)
next_at += step_time
slack = next_at - time.perf_counter()
if slack > 0:
time.sleep(slack)
def move(x: float, y: float, rng: random.Random | None = None) -> None:
"""Move the cursor to (x, y) the way a hand would."""
rng = rng or random.Random()
start = pyautogui.position()
distance = math.hypot(x - start[0], y - start[1])
if distance < 1:
return
duration = _duration_for(distance, rng)
# A long throw usually lands slightly past the mark and gets pulled back.
if distance > OVERSHOOT_ABOVE and rng.random() < OVERSHOOT_CHANCE:
angle = math.atan2(y - start[1], x - start[0]) + rng.uniform(-0.35, 0.35)
past = rng.uniform(6, 16)
overshoot = (x + math.cos(angle) * past, y + math.sin(angle) * past)
_glide(start, overshoot, duration * 0.82, rng)
time.sleep(rng.uniform(0.02, 0.06))
_glide(pyautogui.position(), (x, y), rng.uniform(0.10, 0.19), rng)
else:
_glide(start, (x, y), duration, rng)
# Land exactly on target — accumulated float error must not cost us the click.
pyautogui.moveTo(x, y, duration=0, _pause=False)
def click(x: float, y: float, rng: random.Random | None = None, press: bool = True) -> None:
"""Move to (x, y), settle, then press and release.
With press=False the cursor travels and dwells but no button event is sent.
"""
rng = rng or random.Random()
move(x, y, rng)
# A beat between arriving and pressing: this is what lets hover handlers,
# CSS transitions and lazily-armed controls catch up before the press.
time.sleep(rng.uniform(*DWELL))
if not press:
return
pyautogui.mouseDown(_pause=False)
time.sleep(rng.uniform(*HOLD))
pyautogui.mouseUp(_pause=False)
+6
View File
@@ -0,0 +1,6 @@
pyautogui>=0.9.54
# macOS backends: Quartz drives the mouse, Cocoa (AppKit) raises the browser
# window without needing AppleScript automation permission.
pyobjc-core>=10.0; sys_platform == "darwin"
pyobjc-framework-Quartz>=10.0; sys_platform == "darwin"
pyobjc-framework-Cocoa>=10.0; sys_platform == "darwin"