rust-gui-control
Skill0 starsChanged 3 months agoMITView on GitHub
Use when remote-controlling, driving, testing, or screen-capturing a Rust GUI app on macOS with computer-use or app-control tools — especially when a cargo-built binary draws a window but the controller cannot target it ("Invalid app", missing from app inventory), when pointer/hover actions miss, or when exact-pixel window captures are needed for visual verification.
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 9 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈9
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 36
If before equals after, stop — fix permissions and restart the host first.
- Distinctive: line 40
If the project has a packaging script that accepts a debug binary, use it — it preserves the product's executable name, bundle id, and …
- Distinctive: line 114
Start every session by reading fresh app state.
- Distinctive: line 123
Prefer accessibility element actions over coordinates when elements are exposed.
- Distinctive: line 124
Prefer app-targeted key presses over global keyboard injection.
- Distinctive: line 125
Treat remote click coordinates as relative to the controller's current app screenshot unless the controller documents another space.
- Distinctive: line 130
If the control API lacks pointer movement, in preference order:
- Distinctive: line 132
Use a remote pointer-move/hover action if available.
- Very specific: line 133
Use a debug-only forced-hover hook if the app provides one (e.g.
- Distinctive: line 137
Never claim hover-follows-pointer from a forced-hover screenshot — use at least two real pointer positions or another event-driven check.
- Very specific: line 161
If screencapture -l fails with "could not create image from window", fall back to CGWindowListCreateImage + CGImageDestination.
- Distinctive: line 178
Do not turn debug packaging into a release, signing, tagging, or publishing action.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Remote-controlling Rust GUI apps on macOS
App-control tools can target a Rust GUI only when macOS sees it as an application. A bare cargo run / target/debug/ binary draws a window but has no registered .app identity and is absent from the controller's app inventory.
Workflow: build debug binary → wrap that exact binary in a temporary .app with stable bundle id → target the .app by absolute path → drive keyboard/pointer/menus via app control → use exact-window PNG capture when color/alpha fidelity matters.
macOS permissions
The agent's host process (Terminal, Ghostty, VS Code, Codex, …) needs both:
- System Settings → Privacy & Security → Screen Recording
- System Settings → Privacy & Security → Input Monitoring
Fully quit and restart the host app after granting either — macOS silently drops input events until restart. Verify injection before debugging the Rust app:
uv run --with pyobjc-framework-Quartz python -c "
import Quartz, time
def pos():
point = Quartz.CGEventGetLocation(Quartz.CGEventCreate(None))
return (point.x, point.y)
print('before', pos())
event = Quartz.CGEventCreateMouseEvent(
None, Quartz.kCGEventMouseMoved, (1190, 490), Quartz.kCGMouseButtonLeft)
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
time.sleep(0.5)
print('after ', pos())
"If before equals after, stop — fix permissions and restart the host first.
Preferred: the project's packager
If the project has a packaging script that accepts a debug binary, use it — it preserves the product's executable name, bundle id, and plist:
cargo build -p my-app
python3 tools/package.py --os macos --binary target/debug/my-app --out /tmp/my-app-bundle --version debugPackaging only copies the binary; you are still testing target/debug/my-app. Target the absolute .app path first (ctl = your controller's client object):
var state = await ctl.get_app_state({ app: "/tmp/my-app-bundle/MyApp.app", disableDiff: true });
console.log(state.text);App name or bundle id may work after Launch Services has seen the bundle, but the absolute path is the least ambiguous first target.
Minimum bundle shape (no packager)
MyApp.app/
└── Contents/
├── Info.plist
└── MacOS/
└── my-appInfo.plist minimum:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>my-app</string>
<key>CFBundleIdentifier</key>
<string>dev.example.my-app.debug</string>
<key>CFBundleName</key>
<string>MyApp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
</dict>
</plist>Requirements: CFBundleExecutable exactly matches the file in Contents/MacOS/; executable bit set; bundle id stable and unique; temporary bundles live under a task-specific dir in /tmp, never in the repo or release output.
Build it for a binary at target/debug/my-app:
RUST_GUI_BUNDLE_ROOT="$(mktemp -d /tmp/rust-gui-control.XXXXXX)"
RUST_GUI_APP="$RUST_GUI_BUNDLE_ROOT/MyApp.app"
RUST_GUI_CONTENTS="$RUST_GUI_APP/Contents"
mkdir -p "$RUST_GUI_CONTENTS/MacOS"
cp target/debug/my-app "$RUST_GUI_CONTENTS/MacOS/my-app"
chmod +x "$RUST_GUI_CONTENTS/MacOS/my-app"
plutil -create xml1 "$RUST_GUI_CONTENTS/Info.plist"
/usr/libexec/PlistBuddy \
-c "Add :CFBundleExecutable string my-app" \
-c "Add :CFBundleIdentifier string dev.example.my-app.debug" \
-c "Add :CFBundleName string MyApp" \
-c "Add :CFBundlePackageType string APPL" \
"$RUST_GUI_CONTENTS/Info.plist"
echo "$RUST_GUI_APP"Code signing is not required to remote-control a locally built debug bundle. Signing, entitlements, notarization, and release packaging are separate workflows needing their normal authorization.
Control workflow
Start every session by reading fresh app state; re-read after every action before choosing the next one:
var state = await ctl.get_app_state({ app: appPath, disableDiff: true });
console.log(state.text);
await ctl.press_key({ app: appPath, key: "super+alt+v" });
state = await ctl.get_app_state({ app: appPath });- Prefer accessibility element actions over coordinates when elements are exposed; re-derive element indexes from each new state — they go stale on any UI change.
- Prefer app-targeted key presses over global keyboard injection.
- Treat remote click coordinates as relative to the controller's current app screenshot unless the controller documents another space; capture a fresh full screenshot before coordinate actions.
- Raw custom-rendered surfaces often expose only window + menu bar via accessibility. Coordinate input is expected there — verify success through observable app state (cursor offset, inspector value, selected row) or a follow-up screenshot.
Hover-only verification
If the control API lacks pointer movement, in preference order:
- Use a remote pointer-move/hover action if available.
- Use a debug-only forced-hover hook if the app provides one (e.g.
MYAPP_FORCE_HOVER=0x52). - Raise the bundled app, then inject a Quartz
kCGEventMouseMovedevent. - Click a harmless target only if changing selection/cursor state is acceptable; restore state afterward.
Never claim hover-follows-pointer from a forced-hover screenshot — use at least two real pointer positions or another event-driven check.
Capture workflow
Remote app screenshots are for UI state, accessibility output, and locating controls. They may be JPEG-compressed or diff/mask images — not evidence for subtle alpha, gradients, 1px seams, or exact colors.
For visual-quality verification:
- List windows; record the exact
CGWindowIDand bounds. - Capture that window by ID as PNG; reuse the known ID for later captures.
- Capture only the app window, never the full display.
# list the app's windows, note the CGWindowID
uv run --with pyobjc-framework-Quartz python -c "
import Quartz
for w in Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID):
if w.get('kCGWindowOwnerName') == 'MyApp':
print(w['kCGWindowNumber'], w.get('kCGWindowBounds'))
"
screencapture -l WINDOW_ID -o /tmp/my-app-window.pngIf screencapture -l fails with "could not create image from window", fall back to CGWindowListCreateImage + CGImageDestination. On macOS 15 the SDK marks that API unavailable to direct Swift calls though the runtime symbol exists — bind it dynamically or use a tested project helper.
Troubleshooting
| Symptom | Cause → fix |
Invalid app for a visible Rust window | Bare executable, no bundle → wrap in .app, target absolute bundle path. |
| Controllable but pointer actions miss | Stale state, or wrong coordinate space → refresh state + screenshot; verify whether coordinates are screenshot-relative or global; test on a harmless target. Apply title-bar/shadow/Retina transforms only to global coordinates, never to screenshot-relative ones. |
| Global pointer moves but hover doesn't update | App not frontmost, or no enter/move events → raise app, move from outside the surface inward, wait ≥0.5 s, confirm the point is over real content (custom viewers may paint no hover over EOF/empty space). |
| Screenshot black except changed regions | Controller returned a diff/mask → re-request state with disableDiff: true; if still a mask, use the exact-window PNG path. |
| AppleScript can't activate the app | Bare binaries aren't reliably AppleScript-addressable → bundle first; prefer app-targeted actions. |
| App hangs on first keychain/credential access | Not a control problem — see memento show macos-dev-keychain-prompt-loop and macos-login-keychain-auth-failed (unsigned-binary ACL loops, errSecAuthFailed, first-access signature-eval delay). |
Session cleanup
- Restore any appearance, view mode, wrapping, width, or selection state changed during verification.
- Terminate the temporary app; leave temporary bundles in
/tmp, never commit them. - Do not turn debug packaging into a release, signing, tagging, or publishing action.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

