Expand description
Platform-free core for pixelcoords: geometry, selections, session schema, point verdicts, code emitters, template relocation, hotkey grammar, config, bitmap font, and CPU rasterizer.
The crate README is included below, which makes every example on the crates.io page a compiled doctest — documentation that cannot rot without failing CI.
§pixelcoords-core
The platform-free core of pixelcoords:
the geometry, the session.json schema, template relocation, point
verdicts, and click-code generation — with no window system, no capture
backend, and #![forbid(unsafe_code)].
Want the tool? Install the binary: cargo install pixelcoords.
Want to build something on top of it? That’s this crate.
[dependencies]
pixelcoords-core = "0.1"§What you’d use it for
A pixelcoords session is a human’s answer to “where is this thing on screen, exactly” — labeled regions, pixel-exact, with crops. This crate is everything you need to consume that answer in Rust: read the schema, resolve a label to the point you should click, ask whether a region is still where it was, or check that a coordinate landed inside it.
That covers most tools built here: test harnesses that assert on screen positions, automation runners, screenshot annotators, converters between DPI spaces, and alternative front-ends over the same session format.
You may not need this crate at all. If you only want to read
session.json from another language, the schema is plain JSON and fully
documented in
docs/OUTPUT.md.
Reach for this crate when you want the behavior too — the matching,
the scoring, the geometry — not just the data.
§The coordinate model, which you must get right
Everything else here is downstream of this, and it is the thing tools get wrong.
- A session stores physical pixels, twice per selection:
pxis monitor-local,global_pxis the same region on the global desktop grid. - Each
MonitorRecordcarriesorigin_px(its position on that global grid) andscale(its DPI factor), soglobal = monitor.origin_px + local. - There is no universal logical space, and the schema does not pretend there is. For logical points, divide by the containing monitor’s scale — never a global one. Mixed-DPI desktops are the normal case.
- Input APIs disagree about which space they want: macOS
CGEventtakes logical points; WindowsSendInputand X11XTESTtake physical pixels. Converting to the wrong one clicks the wrong place without erroring.
§Read a session and resolve its click points
The core loop of nearly every consumer: for each labeled region, find the point you would actually click.
use pixelcoords_core::session::SessionFile;
let session: SessionFile = serde_json::from_str(EXAMPLE_SESSION)?;
for selection in &session.selections {
let monitor = session
.monitors
.iter()
.find(|m| m.index == selection.monitor)
.expect("a session describes every monitor it references");
// `click_point` is an interior point of the shape — the centroid for
// a rect, and a point guaranteed *inside* a triangle or freehand
// polygon, where the centroid can fall outside the shape entirely.
let local = selection.px.click_point();
let global_x = monitor.origin_px.x + local.x;
let global_y = monitor.origin_px.y + local.y;
// Logical points, for an API like macOS CGEvent.
let logical_x = f64::from(global_x) / monitor.scale;
let logical_y = f64::from(global_y) / monitor.scale;
println!(
"{}: physical ({global_x}, {global_y}) · logical ({logical_x}, {logical_y})",
selection.label
);
}§Geometry
Shape covers every tool pixelcoords draws — Rect, Circle,
Ellipse, Triangle, and Poly (regular N-gons and freehand alike) —
with an untagged serde representation, so shapes round-trip through JSON
on their own.
use pixelcoords_core::geometry::{Point, Rect, Shape};
let button = Shape::Rect(Rect::new(800, 400, 100, 80));
assert_eq!(button.click_point(), Point::new(850, 440));
assert!(button.hit_test(Point::new(810, 410)));
assert!(!button.hit_test(Point::new(10, 10)));
assert_eq!(button.bbox(), Rect::new(800, 400, 100, 80));
// Rotation is metadata on rects and ellipses, so hit-testing takes the
// angle rather than mutating the shape.
assert!(button.hit_test_rotated(45, button.click_point()));Also here: regular_polygon (N-gon construction from a center and a
point to aim at), simplify_path (Ramer–Douglas–Peucker, for freehand),
rotate_point_about, normalize_deg, and the clamped move/resize
helpers the overlay drives.
§Is this region still where it was?
locate is masked template matching — the engine behind pixelcoords find. Give it a grayscale screen and a template cut from the saved crop,
and it reports where that region is now.
Two constants define the trust model, and they matter more than the
algorithm: SCORE_FLOOR (0.9) is the minimum normalized correlation to
count as found, and AMBIGUITY_GAP (0.03) is how far the best match must
beat the runner-up. A template that matches in two places is reported
as ambiguous rather than picked between — the caller is expected to
refuse, because acting on the wrong instance is worse than not acting.
Template carries an optional mask, so non-rectangular regions match on
the pixels the human actually marked instead of the bounding box.
report() assembles a FindReport — the same JSON pixelcoords find
prints, with per-label Delta values, so you can tell what moved and by
how much.
§Did this point land in the right place?
verdict::assess answers that in whichever space your point already is
— PointSpace::Global, PointSpace::Monitor(i), or PointSpace::Window
for --target sessions.
use pixelcoords_core::geometry::Point;
use pixelcoords_core::session::SessionFile;
use pixelcoords_core::verdict::{PointSpace, assess};
let verdict = assess(&session, Point::new(850, 440), PointSpace::Global, None)?;
assert!(verdict.hit);
assert_eq!(verdict.contained_in[0].label, "submit");
// A miss still reports what you nearly hit, and how far off you were.
let miss = assess(&session, Point::new(1600, 440), PointSpace::Global, None)?;
assert!(!miss.hit);
assert_eq!(miss.nearest.expect("a nearest region").region.label, "submit");contained_in lists every region containing the point, in stacking
order, so overlaps are disambiguated by the caller rather than silently
by the library.
§Generate click code
use pixelcoords_core::emit::{EmitFormat, Platform, emit};
use pixelcoords_core::session::SessionFile;
let snippet = emit(&session, EmitFormat::Pyautogui, Platform::MacOs, None)?;
assert!(snippet.contains("pyautogui"));Platform exists because the same coordinate means different things per
tool: pyautogui makes its process DPI-aware on import, so it addresses
physical pixels on Windows but logical points on macOS. cliclick and
xdotool each exist on one platform and don’t branch.
§The modules, and how much they move
| Module | What it is | Reuse |
|---|---|---|
session | the session.json schema | the contract — versioned, grows additively |
geometry | shapes, hit-testing, rotation, polygon math | stable, pure arithmetic |
locate | masked template matching, scoring, FindReport | stable shape, tuned thresholds |
verdict | point-in-region assessment | stable shape |
emit | pyautogui / cliclick / xdotool generators | stable shape |
selection | the undo/redo edit engine | overlay internals |
draw, font | CPU rasterizer, embedded JetBrains Mono | overlay internals |
hotkeys, config, strings, matcher | binding grammar, strict TOML config, UI text, window-title matching | overlay internals |
Stability, honestly. This is pre-1.0 and shares a version with the
binary, so a minor bump can change any signature here. The session
schema is the part with a real compatibility promise: it is versioned
(SCHEMA_VERSION), additions are optional fields, and consumers that
ignore unknown keys keep working. The top rows are what tools are
actually built on; the overlay internals exist to serve the binary and
move with it. Pin a caret range and read the
CHANGELOG
before upgrading.
Every example above is compiled and run as a doctest in CI, so nothing on this page can rot silently.
§See also
- pixelcoords — the tool this serves
- pixelactions — the executor half: consumes these sessions and performs the interactions
- API docs · repository · pixelcoords.dev
- docs/OUTPUT.md — every JSON shape this crate produces
MIT licensed. Bundled font: JetBrains Mono, OFL 1.1.
Modules§
- config
- Configuration types (serde) and their resolution into validated values.
- draw
- CPU rasterizer: shapes, text, and label placement into a
u32pixel buffer (0x00RRGGBB, softbuffer’s format), plus the alpha mask applied to circle crops. Everything clips to the buffer — drawing partially or fully off-buffer is safe and silent. - emit
- Ready-to-paste click snippets from a session — the logic behind
pixelcoords emit. - font
- Embedded vector font — antialiased text for the overlay.
- geometry
- Shapes and their interaction math, in monitor-local physical pixels.
- hotkeys
- Hotkey binding grammar:
KEY=ACTION[,EDGE][,WHEN]. - locate
- Template relocation: find where a saved crop sits in a fresh capture —
the logic behind
pixelcoords find. - matcher
- Window-target matching for
--target: given the windows visible at freeze time, deterministically pick the one the query means. - selection
- The set of committed selections plus per-op undo and redo stacks.
- session
- The versioned
session.jsonschema. - strings
- User-facing overlay strings. Everything the overlay renders as text
comes from here so a language table can replace
ENlater without a refactor. ASCII only — the embedded font covers printable ASCII. - verdict
- Point-in-region verdicts against a saved session — the logic behind
pixelcoords assert.