plotruler 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plotruler-0.1.3/.github/workflows/publish.yml +51 -0
- plotruler-0.1.3/.gitignore +14 -0
- plotruler-0.1.3/.opencode/opencode.json +4 -0
- plotruler-0.1.3/.opencode/scripts/check.mjs +19 -0
- plotruler-0.1.3/.opencode/scripts/relaunch.mjs +45 -0
- plotruler-0.1.3/.opencode/scripts/verify.mjs +23 -0
- plotruler-0.1.3/AGENTS.md +80 -0
- plotruler-0.1.3/LICENSE +22 -0
- plotruler-0.1.3/PKG-INFO +206 -0
- plotruler-0.1.3/README.md +150 -0
- plotruler-0.1.3/build/build.py +46 -0
- plotruler-0.1.3/build/entry.py +16 -0
- plotruler-0.1.3/build/install_linux.sh +60 -0
- plotruler-0.1.3/build/make_ico.py +64 -0
- plotruler-0.1.3/build/plotruler.desktop +10 -0
- plotruler-0.1.3/build/plotruler.ico +0 -0
- plotruler-0.1.3/build/plotruler.spec +105 -0
- plotruler-0.1.3/build/publish_pypi.sh +30 -0
- plotruler-0.1.3/build/version_info.txt +36 -0
- plotruler-0.1.3/plotruler/__init__.py +3 -0
- plotruler-0.1.3/plotruler/__main__.py +98 -0
- plotruler-0.1.3/plotruler/core.py +371 -0
- plotruler-0.1.3/plotruler/format.py +262 -0
- plotruler-0.1.3/plotruler/hotkey.py +240 -0
- plotruler-0.1.3/plotruler/overlay.py +1135 -0
- plotruler-0.1.3/plotruler/settings.py +130 -0
- plotruler-0.1.3/plotruler/storage.py +144 -0
- plotruler-0.1.3/plotruler/titlebar.py +226 -0
- plotruler-0.1.3/plotruler/tray.py +179 -0
- plotruler-0.1.3/plotruler/win_hittest.py +292 -0
- plotruler-0.1.3/pyproject.toml +67 -0
- plotruler-0.1.3/tests/test_core.py +163 -0
- plotruler-0.1.3/tests/test_format.py +125 -0
- plotruler-0.1.3/tests/test_hotkey.py +112 -0
- plotruler-0.1.3/tests/test_session.py +282 -0
- plotruler-0.1.3/tests/test_storage.py +166 -0
- plotruler-0.1.3/tests/test_win_hittest.py +49 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
name: Build distribution
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v7
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v7
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.x"
|
|
21
|
+
|
|
22
|
+
- name: Install build
|
|
23
|
+
run: pip install build
|
|
24
|
+
|
|
25
|
+
- name: Build wheel and sdist
|
|
26
|
+
run: python -m build
|
|
27
|
+
|
|
28
|
+
- name: Upload dist artifacts
|
|
29
|
+
uses: actions/upload-artifact@v7
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
|
|
34
|
+
publish:
|
|
35
|
+
name: Publish to PyPI
|
|
36
|
+
needs: build
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
|
|
39
|
+
environment: pypi
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write # required for Trusted Publishing
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- name: Download dist artifacts
|
|
45
|
+
uses: actions/download-artifact@v8
|
|
46
|
+
with:
|
|
47
|
+
name: dist
|
|
48
|
+
path: dist/
|
|
49
|
+
|
|
50
|
+
- name: Publish to PyPI
|
|
51
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.pytest_cache/
|
|
4
|
+
.ruff_cache/
|
|
5
|
+
.venv/
|
|
6
|
+
dist/
|
|
7
|
+
# PyInstaller work dir and its generated artifacts live under build/; keep
|
|
8
|
+
# the source (spec, entry, icon, version info) tracked.
|
|
9
|
+
build/plotruler/
|
|
10
|
+
build/__pycache__/
|
|
11
|
+
build/*.toc
|
|
12
|
+
build/*.pkg
|
|
13
|
+
build/*.zip
|
|
14
|
+
*.egg-info/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Offscreen smoke test: import the overlay (which pulls in the Qt-free
|
|
3
|
+
// core, storage, format, tray, hotkey) without showing a window. Catches
|
|
4
|
+
// broken imports and mismatched Qt enums before launching the GUI.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const python = process.env.PYTHON || "python";
|
|
8
|
+
const snippet =
|
|
9
|
+
"import os;" +
|
|
10
|
+
"os.environ['QT_QPA_PLATFORM']='offscreen';" +
|
|
11
|
+
"from plotruler.overlay import OverlayWindow;" +
|
|
12
|
+
"from plotruler import format, storage;" +
|
|
13
|
+
"print('imports OK')";
|
|
14
|
+
|
|
15
|
+
const res = spawnSync(python, ["-c", snippet], {
|
|
16
|
+
stdio: "inherit",
|
|
17
|
+
cwd: process.cwd(),
|
|
18
|
+
});
|
|
19
|
+
process.exit(res.status ?? 1);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Kill any running PlotRuler dev instance and relaunch it.
|
|
3
|
+
//
|
|
4
|
+
// Never blanket-kill python: only processes whose command line mentions
|
|
5
|
+
// 'plotruler' are stopped. The relaunch then starts the app in the
|
|
6
|
+
// foreground. The tray-resident app never exits, so this script blocks
|
|
7
|
+
// until the shell times out — that is expected, not a crash.
|
|
8
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
const python = process.env.PYTHON || "python";
|
|
11
|
+
|
|
12
|
+
function killPlotRuler() {
|
|
13
|
+
const plat = process.platform;
|
|
14
|
+
let list;
|
|
15
|
+
if (plat === "win32") {
|
|
16
|
+
// PowerShell's CIM query returns process ids to kill.
|
|
17
|
+
const ps = spawnSync(
|
|
18
|
+
"powershell.exe",
|
|
19
|
+
[
|
|
20
|
+
"-NoProfile",
|
|
21
|
+
"-Command",
|
|
22
|
+
"Get-CimInstance Win32_Process -Filter \"Name = 'python.exe'\" | " +
|
|
23
|
+
"Where-Object { $_.CommandLine -match 'plotruler' } | " +
|
|
24
|
+
"ForEach-Object { Stop-Process -Id $_.ProcessId -Force }",
|
|
25
|
+
],
|
|
26
|
+
{ stdio: "inherit" },
|
|
27
|
+
);
|
|
28
|
+
return ps.status ?? 0;
|
|
29
|
+
}
|
|
30
|
+
// Non-Windows: pkill matching the package path.
|
|
31
|
+
const res = spawnSync("pkill", ["-f", "plotruler"], { stdio: "ignore" });
|
|
32
|
+
// pkill returns 1 when nothing matched, which is fine.
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
killPlotRuler();
|
|
37
|
+
// Give the OS a moment to release the socket/process before relaunching.
|
|
38
|
+
const wait = spawnSync("node", ["-e", "setTimeout(()=>{}, 1000)"], {
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
});
|
|
41
|
+
const app = spawn(python, ["-m", "plotruler"], {
|
|
42
|
+
stdio: "inherit",
|
|
43
|
+
cwd: process.cwd(),
|
|
44
|
+
});
|
|
45
|
+
app.on("exit", (code) => process.exit(code ?? 0));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Lint, format-check, and run the test suite. Exits nonzero on failure.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const steps = [
|
|
6
|
+
["ruff lint", ["-m", "ruff", "check", "."]],
|
|
7
|
+
["ruff format check", ["-m", "ruff", "format", "--check", "."]],
|
|
8
|
+
["pytest", ["-m", "pytest", "-q"]],
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
// Run the conda env's python as-is (assuming it is already active).
|
|
12
|
+
const python = process.env.PYTHON || "python";
|
|
13
|
+
|
|
14
|
+
let ok = true;
|
|
15
|
+
for (const [label, args] of steps) {
|
|
16
|
+
const res = spawnSync(python, args, { stdio: "inherit", cwd: process.cwd() });
|
|
17
|
+
if (res.status !== 0) {
|
|
18
|
+
console.error(`\nFAILED: ${label} (exit ${res.status})`);
|
|
19
|
+
ok = false;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# PlotRuler
|
|
2
|
+
|
|
3
|
+
PlotRuler is a **cross-platform** desktop overlay for reading (X, Y) values off a graph shown on screen. Calibrate once against a known plot (click two points per axis, type their values), then hover to read coordinates and click to copy.
|
|
4
|
+
|
|
5
|
+
The graph itself is **never rendered by PlotRuler** — it is whatever other app is on screen underneath the overlay (a browser, a PDF viewer, a plotting window). PlotRuler is a **live translucent overlay** that sits on top and reads through it, not a screenshot/frozen-image workflow. Calibration persists to disk across sessions and across hide/show.
|
|
6
|
+
|
|
7
|
+
Stack: Python 3.14 (conda env `plotruler`) · PySide6 6.11.2 (Qt) · pytest. The app is tray-resident; **Win+Alt+P** toggles the overlay.
|
|
8
|
+
|
|
9
|
+
## Quick commands
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
conda activate plotruler
|
|
13
|
+
python -m plotruler # run the app
|
|
14
|
+
pytest # run tests
|
|
15
|
+
ruff check . # lint
|
|
16
|
+
ruff format . # auto-format
|
|
17
|
+
python build/build.py # build dist/PlotRuler.exe (requires pyinstaller)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Environment
|
|
21
|
+
|
|
22
|
+
- Work only in the conda env `plotruler` (Python 3.14). Never touch the system Python.
|
|
23
|
+
- Dependencies and tool config live in **`pyproject.toml`** (PEP 621, hatchling build backend). No `requirements.txt`.
|
|
24
|
+
- Windows is the primary platform, but the app is meant to be **cross-platform and useful to other people**. Keep the math and calibration model platform-agnostic; the overlay/tray/hotkey shell is Windows-first and may use small Win32 shims where needed.
|
|
25
|
+
|
|
26
|
+
## Code conventions
|
|
27
|
+
|
|
28
|
+
- **No type hints.** The owner prefers plain old-school Python. Clarity, simple names, and docstrings over annotations. (The codebase is read by humans, not type-checked.)
|
|
29
|
+
- Comments explain **why**, not what. If a comment would just restate the code, rewrite the code to be clearer instead. Never delete or omit comments while editing.
|
|
30
|
+
- Line length 79 (PEP8), enforced by ruff.
|
|
31
|
+
- Pythonic and simple. Write code a reader can follow without mental gymnastics; favor the clear, straightforward approach over clever one-liners and premature optimization.
|
|
32
|
+
- Architecture: the pixel→value calibration math must stay **Qt-free** (importable without PySide6) so it stays unit-testable and portable. Everything else (overlay, calibration UI, tray, hotkeys) is ordinary Qt code. No artificial core/shell package split beyond that.
|
|
33
|
+
|
|
34
|
+
## GUI conventions
|
|
35
|
+
|
|
36
|
+
- One frameless translucent window. Title bar, crosshair, readout, and instruction box are custom-painted with QPainter — there are no native widgets.
|
|
37
|
+
- Calibration is stored in **absolute screen coordinates** (physical pixels). Moving or resizing the overlay never invalidates a calibration.
|
|
38
|
+
- DPI-aware: Qt high-DPI scaling is on, and the math must stay correct at 100% and 150% display scaling. This silently corrupting values would be the worst failure mode.
|
|
39
|
+
|
|
40
|
+
## Testing
|
|
41
|
+
|
|
42
|
+
- pytest covers the math (`tests/test_*.py`). Every test function needs a docstring stating what behavior it verifies and why. New functions get tests; bug fixes get regression tests.
|
|
43
|
+
- The GUI is **not unit-tested** — translucent always-on-top overlays are interaction- heavy and don't test well without a running event loop. Keep widgets thin (logic in testable functions), and verify the GUI by running it. pytest-qt can be added later if a real need shows up.
|
|
44
|
+
- Give testing instructions to the user *before* running the program.
|
|
45
|
+
|
|
46
|
+
## Hotkey
|
|
47
|
+
|
|
48
|
+
- **Win+Alt+P** is the default summon/toggle hotkey (P for PlotRuler). It is configurable — this is required, not optional. It is unclaimed on stock Windows; the only known collision is PowerToys' optional "Mouse Pointer Crosshairs", and Win+Ctrl+P is a free alternative if that matters. Avoid plain Ctrl+Alt+Space — Visual Studio and ReSharper both bind it, so IDE users would have to remap.
|
|
49
|
+
- **Ctrl+N** (new calibration) and **Esc** (hide) are in-app keys, not global.
|
|
50
|
+
|
|
51
|
+
## Commits
|
|
52
|
+
|
|
53
|
+
- Conventional Commits: `feat:`, `fix:`, `test:`, `refactor:`, `docs:`, `chore:`.
|
|
54
|
+
- Small, atomic commits: one coherent idea plus its tests and docs — nothing else.
|
|
55
|
+
- Comprehensive messages: subject summarizes; body explains the problem, the approach, and any trade-offs.
|
|
56
|
+
- Add the trailer `Co-authored-by: opencode <opencode@anomalyco.ai>` to every commit.
|
|
57
|
+
- Never commit agent-generated scratch files (summary notes, session dumps, chat exports).
|
|
58
|
+
|
|
59
|
+
## Versioning
|
|
60
|
+
|
|
61
|
+
- **Semantic Versioning** (MAJOR.MINOR.PATCH). Bump MAJOR on breaking changes, MINOR on new features, PATCH on fixes; pre-release tags (`-rc1`, etc.) are allowed when preparing a release.
|
|
62
|
+
- The version lives in **three places** that must be bumped together in the same commit:
|
|
63
|
+
- `pyproject.toml` → `[project] version`
|
|
64
|
+
- `plotruler/__init__.py` → `__version__`
|
|
65
|
+
- `build/version_info.txt` → the `filevers`/`prodvers` tuples and the `FileVersion`/`ProductVersion` strings
|
|
66
|
+
- Docstring on the entry point or README may mention the version, but only as a mirror of these three.
|
|
67
|
+
- The version in the code is the source of truth; tag a release with a matching `vX.Y.Z` when cutting one.
|
|
68
|
+
|
|
69
|
+
## Docs
|
|
70
|
+
|
|
71
|
+
- Update the README when user-visible behavior changes.
|
|
72
|
+
- Update this AGENTS.md when the development guidelines change.
|
|
73
|
+
|
|
74
|
+
## Scope (current)
|
|
75
|
+
|
|
76
|
+
MVP is: live translucent overlay; linear-rectangle calibration only (X axis then Y axis, two click-points each); hover-to-read; click-to-copy with "copied" confirmation; custom always-visible translucent title bar with resize handles; calibration + window position persistence; tray icon and global hotkey.
|
|
77
|
+
|
|
78
|
+
Deferred: corner/homography mode, log axes, X-only/Y-only modes, pins/slope readout, CSV digitizing.
|
|
79
|
+
|
|
80
|
+
`spec.md` is the product reference. When it conflicts with decisions recorded here, this file wins — the spec was AI-written and worded more rigidly than the product actually is.
|
plotruler-0.1.3/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 endolith
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
plotruler-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: plotruler
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Read (X, Y) values off an on-screen graph with a translucent overlay
|
|
5
|
+
Project-URL: Homepage, https://github.com/endolith/plotruler
|
|
6
|
+
Project-URL: Repository, https://github.com/endolith/plotruler
|
|
7
|
+
Project-URL: Issues, https://github.com/endolith/plotruler/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/endolith/plotruler/releases
|
|
9
|
+
Author: endolith
|
|
10
|
+
Maintainer: endolith
|
|
11
|
+
License: MIT License
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2026 endolith
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
|
32
|
+
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Keywords: calibration,digitize,digitizer,graph,overlay,plot,readout
|
|
35
|
+
Classifier: Development Status :: 4 - Beta
|
|
36
|
+
Classifier: Environment :: X11 Applications
|
|
37
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
38
|
+
Classifier: Intended Audience :: Science/Research
|
|
39
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
40
|
+
Classifier: Operating System :: MacOS
|
|
41
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
42
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
43
|
+
Classifier: Programming Language :: Python :: 3
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
49
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
50
|
+
Requires-Python: >=3.10
|
|
51
|
+
Requires-Dist: pyside6==6.11.2
|
|
52
|
+
Provides-Extra: dev
|
|
53
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
54
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
55
|
+
Description-Content-Type: text/markdown
|
|
56
|
+
|
|
57
|
+
# PlotRuler
|
|
58
|
+
|
|
59
|
+
A translucent desktop overlay for accurately reading (X, Y) values off a graph
|
|
60
|
+
shown on screen. Calibrate once against the known plot coordinates (click at two
|
|
61
|
+
points on each axis, and type their labeled values), then hover to read
|
|
62
|
+
coordinates anywhere else, and click to copy.
|
|
63
|
+
|
|
64
|
+
The graph itself is not rendered by PlotRuler — it is whatever other app is on
|
|
65
|
+
screen underneath the overlay (a browser, a PDF viewer, a plotting window, etc.)
|
|
66
|
+
PlotRuler is a **live translucent overlay** that reads through it, not a
|
|
67
|
+
screenshot workflow.
|
|
68
|
+
|
|
69
|
+
## How it works
|
|
70
|
+
|
|
71
|
+
Calibration and readout all live in **absolute screen coordinates** (physical
|
|
72
|
+
pixels), not coordinates relative to the overlay window. That is what makes the
|
|
73
|
+
overlay feel stable: you can drag or resize the window and the calibrated graph
|
|
74
|
+
box stays glued to the graph underneath wherever it sits on screen. Windows and
|
|
75
|
+
macOS get this natively; Linux/X11 uses the same absolute-coordinate model.
|
|
76
|
+
|
|
77
|
+
Each axis is calibrated independently with two reference points — click once at
|
|
78
|
+
a known pixel on the axis, type its labeled value, then repeat for a second
|
|
79
|
+
point. PlotRuler fits a line through those pairs (linear by default, optional
|
|
80
|
+
log), so it works for any graph where the scale is straight-line or log.
|
|
81
|
+
Everything persists to disk (calibration, window position, number format), so
|
|
82
|
+
the overlay reopens already calibrated.
|
|
83
|
+
|
|
84
|
+
## Features
|
|
85
|
+
|
|
86
|
+
- **Hover to read** — a crosshair and a (X, Y) readout follow the cursor once
|
|
87
|
+
calibrated, showing the values under it.
|
|
88
|
+
- **Click to copy** — a click copies the hovered coordinate as e.g.
|
|
89
|
+
`(12.5, 4.0)` and flashes a confirmation.
|
|
90
|
+
- **Number formats** — plain, scientific, engineering, E-notation, SI
|
|
91
|
+
(auto/1..6 via the number keys, or the tray menu).
|
|
92
|
+
- **Calibration** — Ctrl+N starts a fresh one; Ctrl+Z undoes a step; Esc
|
|
93
|
+
cancels. Each axis gets its own linear/log choice.
|
|
94
|
+
- **Custom frameless title bar** — translucent, with minimize, maximize, and
|
|
95
|
+
(on no-tray systems) a close button.
|
|
96
|
+
- **Tray resident** — sits in the system tray; tray click toggles the overlay.
|
|
97
|
+
|
|
98
|
+
## Platform comparison
|
|
99
|
+
|
|
100
|
+
| | Windows | macOS | Linux/X11 |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| Absolute screen coordinates | ✅ native | ✅ native | ✅ (same model) |
|
|
103
|
+
| Overlay readout through the graph | ✅ | ✅ | ✅ |
|
|
104
|
+
| Move / resize the window | ✅ native | ✅ native | ✅ Qt-driven |
|
|
105
|
+
| Calibration survives window move/resize | ✅ | ✅ | ✅ |
|
|
106
|
+
| Global hotkey (Win+Alt+P / Cmd+Alt+P) | ✅ RegisterHotKey | ⏳ | ⏳ deferred (XGrabKey) |
|
|
107
|
+
| System tray | ✅ | ✅ | ⚠️ depends on DE |
|
|
108
|
+
| Build/ship | .exe (PyInstaller) | ⏳ untested | pip / PyPI wheel |
|
|
109
|
+
|
|
110
|
+
**Wayland is not supported yet.** Wayland forbids absolute screen coordinates by
|
|
111
|
+
design and GNOME refuses the workarounds, so the overlay model cannot work there
|
|
112
|
+
without a window-relative rewrite. Linux requires an **X11** session for now;
|
|
113
|
+
see `LINUX_PLAN.md`.
|
|
114
|
+
|
|
115
|
+
Notes on the platform table:
|
|
116
|
+
- **macOS is untested.** The architecture is OS-portable (absolute screen
|
|
117
|
+
coordinates, Qt overlay), and Qt provides a native macOS path, but no macOS
|
|
118
|
+
build or hotkey code exists in this repo yet. Treat the macOS column as a
|
|
119
|
+
design feature, not a shipped one.
|
|
120
|
+
- **Global hotkey** — Windows-only today. On Linux the tray (or Ctrl+N/Esc and
|
|
121
|
+
the title-bar buttons) are the controls; an X11 `XGrabKey` hotkey is a
|
|
122
|
+
follow-up.
|
|
123
|
+
- **System tray** — always present on Windows; on Linux it depends on the
|
|
124
|
+
desktop environment. GNOME has no tray by default unless the *AppIndicator
|
|
125
|
+
and KStatusNotifier* extension is installed. When no tray exists, PlotRuler
|
|
126
|
+
shows a close button and quits on minimize/close/Esc rather than hiding into
|
|
127
|
+
an unreachable state.
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
conda activate plotruler
|
|
133
|
+
python -m plotruler # run the app
|
|
134
|
+
pytest # run tests
|
|
135
|
+
ruff check . # lint
|
|
136
|
+
ruff format . # auto-format
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Linux
|
|
140
|
+
|
|
141
|
+
Linux requires an X11 session (Wayland is deferred — see above). Qt 6.5+
|
|
142
|
+
also needs the `xcb-cursor` system library for the X11 backend, which pip
|
|
143
|
+
cannot install:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
# Debian / Ubuntu
|
|
147
|
+
sudo apt install libxcb-cursor0
|
|
148
|
+
# Fedora / RHEL
|
|
149
|
+
sudo dnf install xcb-util-cursor
|
|
150
|
+
# Arch
|
|
151
|
+
sudo pacman -S xcb-util-cursor
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Install and run (no frozen binary on Linux):
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
pip install plotruler # or, from source: pip install -e . then plotruler
|
|
158
|
+
plotruler
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> PyPI/wheels cannot install system libraries like `libxcb-cursor`; without it
|
|
162
|
+
> the app aborts at first launch. Install it first (commands above). The
|
|
163
|
+
> `build/install_linux.sh` helper also checks for it and fails fast with your
|
|
164
|
+
> distro's package name.
|
|
165
|
+
|
|
166
|
+
## Releases & publishing
|
|
167
|
+
|
|
168
|
+
PlotRuler ships through **two channels**:
|
|
169
|
+
|
|
170
|
+
| Channel | Artifact | Who uses it |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| **PyPI** (`pip install plotruler`) | wheel (`.whl`) | everyone, all platforms; the Linux and pip path |
|
|
173
|
+
| **GitHub Releases** | `PlotRuler.exe` | Windows users who don't want pip |
|
|
174
|
+
|
|
175
|
+
Publish a Python release to PyPI:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
pip install build twine
|
|
179
|
+
./build/publish_pypi.sh --check # first: upload to TestPyPI and verify
|
|
180
|
+
./build/publish_pypi.sh # then: upload to PyPI
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Requires `TWINE_USERNAME`/`TWINE_PASSWORD` (or a `~/.pypirc`), and an account
|
|
184
|
+
with upload rights for the `plotruler` name. The package supports Python 3.10+
|
|
185
|
+
(down to PySide6 6.11's own floor).
|
|
186
|
+
|
|
187
|
+
The Windows `.exe` is separate and must be built on Windows.
|
|
188
|
+
|
|
189
|
+
## Build a stand-alone Windows executable
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
python -m pip install pyinstaller
|
|
193
|
+
python build/build.py
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Produces a single-file, windowed `dist/PlotRuler.exe` with a tray icon and
|
|
197
|
+
version metadata. The app bundles Qt, so the exe is ~46 MB but needs no Python
|
|
198
|
+
install to run. This is the artifact you attach to a GitHub release.
|
|
199
|
+
|
|
200
|
+
See `AGENTS.md` for development conventions.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT License (see `LICENSE`).
|
|
205
|
+
|
|
206
|
+
(This was generated almost entirely by AI under human direction, so likely lacks the human authorship required for copyright in the US and is therefore in the public domain. MIT license applies anywhere else.)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# PlotRuler
|
|
2
|
+
|
|
3
|
+
A translucent desktop overlay for accurately reading (X, Y) values off a graph
|
|
4
|
+
shown on screen. Calibrate once against the known plot coordinates (click at two
|
|
5
|
+
points on each axis, and type their labeled values), then hover to read
|
|
6
|
+
coordinates anywhere else, and click to copy.
|
|
7
|
+
|
|
8
|
+
The graph itself is not rendered by PlotRuler — it is whatever other app is on
|
|
9
|
+
screen underneath the overlay (a browser, a PDF viewer, a plotting window, etc.)
|
|
10
|
+
PlotRuler is a **live translucent overlay** that reads through it, not a
|
|
11
|
+
screenshot workflow.
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
Calibration and readout all live in **absolute screen coordinates** (physical
|
|
16
|
+
pixels), not coordinates relative to the overlay window. That is what makes the
|
|
17
|
+
overlay feel stable: you can drag or resize the window and the calibrated graph
|
|
18
|
+
box stays glued to the graph underneath wherever it sits on screen. Windows and
|
|
19
|
+
macOS get this natively; Linux/X11 uses the same absolute-coordinate model.
|
|
20
|
+
|
|
21
|
+
Each axis is calibrated independently with two reference points — click once at
|
|
22
|
+
a known pixel on the axis, type its labeled value, then repeat for a second
|
|
23
|
+
point. PlotRuler fits a line through those pairs (linear by default, optional
|
|
24
|
+
log), so it works for any graph where the scale is straight-line or log.
|
|
25
|
+
Everything persists to disk (calibration, window position, number format), so
|
|
26
|
+
the overlay reopens already calibrated.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **Hover to read** — a crosshair and a (X, Y) readout follow the cursor once
|
|
31
|
+
calibrated, showing the values under it.
|
|
32
|
+
- **Click to copy** — a click copies the hovered coordinate as e.g.
|
|
33
|
+
`(12.5, 4.0)` and flashes a confirmation.
|
|
34
|
+
- **Number formats** — plain, scientific, engineering, E-notation, SI
|
|
35
|
+
(auto/1..6 via the number keys, or the tray menu).
|
|
36
|
+
- **Calibration** — Ctrl+N starts a fresh one; Ctrl+Z undoes a step; Esc
|
|
37
|
+
cancels. Each axis gets its own linear/log choice.
|
|
38
|
+
- **Custom frameless title bar** — translucent, with minimize, maximize, and
|
|
39
|
+
(on no-tray systems) a close button.
|
|
40
|
+
- **Tray resident** — sits in the system tray; tray click toggles the overlay.
|
|
41
|
+
|
|
42
|
+
## Platform comparison
|
|
43
|
+
|
|
44
|
+
| | Windows | macOS | Linux/X11 |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| Absolute screen coordinates | ✅ native | ✅ native | ✅ (same model) |
|
|
47
|
+
| Overlay readout through the graph | ✅ | ✅ | ✅ |
|
|
48
|
+
| Move / resize the window | ✅ native | ✅ native | ✅ Qt-driven |
|
|
49
|
+
| Calibration survives window move/resize | ✅ | ✅ | ✅ |
|
|
50
|
+
| Global hotkey (Win+Alt+P / Cmd+Alt+P) | ✅ RegisterHotKey | ⏳ | ⏳ deferred (XGrabKey) |
|
|
51
|
+
| System tray | ✅ | ✅ | ⚠️ depends on DE |
|
|
52
|
+
| Build/ship | .exe (PyInstaller) | ⏳ untested | pip / PyPI wheel |
|
|
53
|
+
|
|
54
|
+
**Wayland is not supported yet.** Wayland forbids absolute screen coordinates by
|
|
55
|
+
design and GNOME refuses the workarounds, so the overlay model cannot work there
|
|
56
|
+
without a window-relative rewrite. Linux requires an **X11** session for now;
|
|
57
|
+
see `LINUX_PLAN.md`.
|
|
58
|
+
|
|
59
|
+
Notes on the platform table:
|
|
60
|
+
- **macOS is untested.** The architecture is OS-portable (absolute screen
|
|
61
|
+
coordinates, Qt overlay), and Qt provides a native macOS path, but no macOS
|
|
62
|
+
build or hotkey code exists in this repo yet. Treat the macOS column as a
|
|
63
|
+
design feature, not a shipped one.
|
|
64
|
+
- **Global hotkey** — Windows-only today. On Linux the tray (or Ctrl+N/Esc and
|
|
65
|
+
the title-bar buttons) are the controls; an X11 `XGrabKey` hotkey is a
|
|
66
|
+
follow-up.
|
|
67
|
+
- **System tray** — always present on Windows; on Linux it depends on the
|
|
68
|
+
desktop environment. GNOME has no tray by default unless the *AppIndicator
|
|
69
|
+
and KStatusNotifier* extension is installed. When no tray exists, PlotRuler
|
|
70
|
+
shows a close button and quits on minimize/close/Esc rather than hiding into
|
|
71
|
+
an unreachable state.
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
conda activate plotruler
|
|
77
|
+
python -m plotruler # run the app
|
|
78
|
+
pytest # run tests
|
|
79
|
+
ruff check . # lint
|
|
80
|
+
ruff format . # auto-format
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Linux
|
|
84
|
+
|
|
85
|
+
Linux requires an X11 session (Wayland is deferred — see above). Qt 6.5+
|
|
86
|
+
also needs the `xcb-cursor` system library for the X11 backend, which pip
|
|
87
|
+
cannot install:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
# Debian / Ubuntu
|
|
91
|
+
sudo apt install libxcb-cursor0
|
|
92
|
+
# Fedora / RHEL
|
|
93
|
+
sudo dnf install xcb-util-cursor
|
|
94
|
+
# Arch
|
|
95
|
+
sudo pacman -S xcb-util-cursor
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Install and run (no frozen binary on Linux):
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
pip install plotruler # or, from source: pip install -e . then plotruler
|
|
102
|
+
plotruler
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
> PyPI/wheels cannot install system libraries like `libxcb-cursor`; without it
|
|
106
|
+
> the app aborts at first launch. Install it first (commands above). The
|
|
107
|
+
> `build/install_linux.sh` helper also checks for it and fails fast with your
|
|
108
|
+
> distro's package name.
|
|
109
|
+
|
|
110
|
+
## Releases & publishing
|
|
111
|
+
|
|
112
|
+
PlotRuler ships through **two channels**:
|
|
113
|
+
|
|
114
|
+
| Channel | Artifact | Who uses it |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| **PyPI** (`pip install plotruler`) | wheel (`.whl`) | everyone, all platforms; the Linux and pip path |
|
|
117
|
+
| **GitHub Releases** | `PlotRuler.exe` | Windows users who don't want pip |
|
|
118
|
+
|
|
119
|
+
Publish a Python release to PyPI:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
pip install build twine
|
|
123
|
+
./build/publish_pypi.sh --check # first: upload to TestPyPI and verify
|
|
124
|
+
./build/publish_pypi.sh # then: upload to PyPI
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Requires `TWINE_USERNAME`/`TWINE_PASSWORD` (or a `~/.pypirc`), and an account
|
|
128
|
+
with upload rights for the `plotruler` name. The package supports Python 3.10+
|
|
129
|
+
(down to PySide6 6.11's own floor).
|
|
130
|
+
|
|
131
|
+
The Windows `.exe` is separate and must be built on Windows.
|
|
132
|
+
|
|
133
|
+
## Build a stand-alone Windows executable
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
python -m pip install pyinstaller
|
|
137
|
+
python build/build.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Produces a single-file, windowed `dist/PlotRuler.exe` with a tray icon and
|
|
141
|
+
version metadata. The app bundles Qt, so the exe is ~46 MB but needs no Python
|
|
142
|
+
install to run. This is the artifact you attach to a GitHub release.
|
|
143
|
+
|
|
144
|
+
See `AGENTS.md` for development conventions.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT License (see `LICENSE`).
|
|
149
|
+
|
|
150
|
+
(This was generated almost entirely by AI under human direction, so likely lacks the human authorship required for copyright in the US and is therefore in the public domain. MIT license applies anywhere else.)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Build the PlotRuler executable.
|
|
2
|
+
|
|
3
|
+
Regenerates the multi-size .ico from the tray icon, then runs PyInstaller
|
|
4
|
+
with the project's spec. Run from the repo root:
|
|
5
|
+
|
|
6
|
+
python build/build.py
|
|
7
|
+
|
|
8
|
+
Requires PyInstaller installed in the active environment:
|
|
9
|
+
python -m pip install pyinstaller
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
17
|
+
BUILD = os.path.join(ROOT, "build")
|
|
18
|
+
sys.path.insert(0, ROOT)
|
|
19
|
+
|
|
20
|
+
import make_ico # noqa: E402 (regenerate the .ico first)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
# Render the .ico (needs a QApplication) before delegating to PyInstaller.
|
|
25
|
+
from PySide6.QtWidgets import QApplication
|
|
26
|
+
|
|
27
|
+
app = QApplication([])
|
|
28
|
+
make_ico.build_ico(app, os.path.join(BUILD, "plotruler.ico"))
|
|
29
|
+
app.quit()
|
|
30
|
+
|
|
31
|
+
cmd = [
|
|
32
|
+
sys.executable,
|
|
33
|
+
"-m",
|
|
34
|
+
"PyInstaller",
|
|
35
|
+
"--noconfirm",
|
|
36
|
+
"--clean",
|
|
37
|
+
"--distpath",
|
|
38
|
+
os.path.join(ROOT, "dist"),
|
|
39
|
+
os.path.join(BUILD, "plotruler.spec"),
|
|
40
|
+
]
|
|
41
|
+
print("running:", " ".join(cmd))
|
|
42
|
+
subprocess.run(cmd, cwd=ROOT, check=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|