plotruler 0.1.3__py3-none-any.whl

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.
@@ -0,0 +1,292 @@
1
+ """Win32 shim that gives the frameless overlay native window behavior.
2
+
3
+ A frameless window has no native frame, so Windows refuses to move,
4
+ resize, or snap it, and external tools like GridMove cannot grab it. On
5
+ its own, answering WM_NCHITTEST with HTCAPTION gives dragging but not
6
+ snapping, because Qt's FramelessWindowHint creates the window as
7
+ WS_POPUP and the OS will not run its modal move loop for such a window.
8
+
9
+ The fix has three parts, all in this module:
10
+
11
+ 1. Give the window the native overlapped-window style bits (WS_CAPTION,
12
+ WS_THICKFRAME, WS_MAXIMIZEBOX, ...) so Windows treats it as an
13
+ ordinary application window. This is what makes it snappable.
14
+ 2. Hide the actual frame by answering WM_NCCALCSIZE and collapsing the
15
+ non-client area, so the native title bar and borders are never drawn.
16
+ 3. Answer WM_NCHITTEST so the custom title bar behaves as a caption and
17
+ the edges resize natively, and WM_GETMINMAXINFO so the window's
18
+ minimum size is respected and maximizing fills the work area exactly.
19
+
20
+ Windows-only. On other platforms every function is a no-op.
21
+ """
22
+
23
+ import ctypes
24
+ import sys
25
+
26
+ try:
27
+ from ctypes import wintypes
28
+ except (ImportError, OSError):
29
+ wintypes = None
30
+
31
+ from PySide6.QtCore import QPoint
32
+
33
+ # ctypes.wintypes imports fine on every platform (it is only a module of
34
+ # struct types); ctypes.windll is what is Windows-only. So wintypes being
35
+ # non-None is not proof we are on Windows, and a windll call on Linux dies
36
+ # with AttributeError. Gate every Win32 path in this file on the real OS.
37
+ _IS_WINDOWS = sys.platform == "win32" and wintypes is not None
38
+
39
+ debug_enabled = False
40
+
41
+
42
+ def _debug(msg):
43
+ if debug_enabled:
44
+ import sys
45
+
46
+ print("[hittest]", msg, file=sys.stderr, flush=True)
47
+
48
+
49
+ WM_NCHITTEST = 0x0084
50
+ WM_NCLBUTTONDBLCLK = 0x00A3
51
+ WM_NCCALCSIZE = 0x0083
52
+ WM_GETMINMAXINFO = 0x0024
53
+
54
+ HTCLIENT = 1
55
+ HTCAPTION = 2
56
+ HTLEFT = 10
57
+ HTRIGHT = 11
58
+ HTTOP = 12
59
+ HTTOPLEFT = 13
60
+ HTTOPRIGHT = 14
61
+ HTBOTTOM = 15
62
+ HTBOTTOMLEFT = 16
63
+ HTBOTTOMRIGHT = 17
64
+
65
+ GWL_STYLE = -16
66
+ GWL_EXSTYLE = -20
67
+ WS_POPUP = 0x80000000
68
+ WS_CAPTION = 0x00C00000
69
+ WS_THICKFRAME = 0x00040000
70
+ WS_SYSMENU = 0x00080000
71
+ WS_MINIMIZEBOX = 0x00020000
72
+ WS_MAXIMIZEBOX = 0x00010000
73
+ WS_OVERLAPPEDWINDOW = (
74
+ WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
75
+ )
76
+ WS_EX_TOOLWINDOW = 0x00000080
77
+ WS_EX_TOPMOST = 0x00000008
78
+ WS_EX_LAYERED = 0x00080000
79
+
80
+ SWP_NOSIZE = 0x0001
81
+ SWP_NOMOVE = 0x0002
82
+ SWP_NOZORDER = 0x0004
83
+ SWP_NOACTIVATE = 0x0010
84
+ SWP_FRAMECHANGED = 0x0020
85
+
86
+ # Window placement show commands for WINDOWPLACEMENT.showCmd.
87
+ SW_HIDE = 0
88
+ SW_SHOW = 5
89
+ SW_SHOWNORMAL = 1
90
+ SW_SHOWMAXIMIZED = 3
91
+
92
+
93
+ class MINMAXINFO(ctypes.Structure):
94
+ _fields_ = [
95
+ ("ptReserved", wintypes.POINT),
96
+ ("ptMaxSize", wintypes.POINT),
97
+ ("ptMaxPosition", wintypes.POINT),
98
+ ("ptMinTrackSize", wintypes.POINT),
99
+ ("ptMaxTrackSize", wintypes.POINT),
100
+ ]
101
+
102
+
103
+ class NCCALCSIZE_PARAMS(ctypes.Structure):
104
+ _fields_ = [
105
+ ("rgrc", wintypes.RECT * 3),
106
+ ("lppos", ctypes.c_void_p),
107
+ ]
108
+
109
+
110
+ def _local_point(window, lparam):
111
+ """Convert a WM_* cursor lParam to the window's local coordinates."""
112
+ x = ctypes.c_short(lparam & 0xFFFF).value
113
+ y = ctypes.c_short((lparam >> 16) & 0xFFFF).value
114
+ # lParam is in physical screen pixels; Qt geometry is logical, so
115
+ # scale down before mapping into the widget.
116
+ dpr = window.devicePixelRatioF()
117
+ logical = QPoint(int(x / dpr), int(y / dpr))
118
+ return window.mapFromGlobal(logical)
119
+
120
+
121
+ def window_rect(window):
122
+ """Return the window's on-screen rect in physical pixels, or None.
123
+
124
+ Uses GetWindowRect, which reports the actual displayed bounds. This
125
+ matters for a maximized or aero-snapped window, where Qt's geometry()
126
+ returns the pre-snap "restore" rect rather than where the window is
127
+ really sitting on screen.
128
+ """
129
+ if not _IS_WINDOWS:
130
+ return None
131
+ try:
132
+ hwnd = int(window.winId())
133
+ rect = wintypes.RECT()
134
+ if not ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect)):
135
+ return None
136
+ return rect.left, rect.top, rect.right, rect.bottom
137
+ except Exception:
138
+ return None
139
+
140
+
141
+ class WINDOWPLACEMENT(ctypes.Structure):
142
+ _fields_ = [
143
+ ("length", wintypes.UINT),
144
+ ("flags", wintypes.UINT),
145
+ ("showCmd", wintypes.UINT),
146
+ ("ptMinPosition", wintypes.POINT),
147
+ ("ptMaxPosition", wintypes.POINT),
148
+ ("rcNormalPosition", wintypes.RECT),
149
+ ]
150
+
151
+
152
+ def set_window_rect(window, rect, show=SW_SHOW):
153
+ """Place the window at a physical (left, top, right, bottom) rect.
154
+
155
+ Uses SetWindowPlacement, which sets the bounds and the window state
156
+ together. That is what makes a snapped window restore correctly: a
157
+ plain move/resize after show() is overridden by Windows re-applying
158
+ its stored snap restore position, whereas SetWindowPlacement overrides
159
+ that stored position in one atomic call.
160
+ """
161
+ if not _IS_WINDOWS:
162
+ return
163
+ try:
164
+ hwnd = int(window.winId())
165
+ left, top, right, bottom = rect
166
+ placement = WINDOWPLACEMENT()
167
+ placement.length = ctypes.sizeof(WINDOWPLACEMENT)
168
+ placement.flags = 0
169
+ placement.showCmd = show
170
+ placement.ptMinPosition.x = 0
171
+ placement.ptMinPosition.y = 0
172
+ placement.ptMaxPosition.x = 0
173
+ placement.ptMaxPosition.y = 0
174
+ placement.rcNormalPosition.left = left
175
+ placement.rcNormalPosition.top = top
176
+ placement.rcNormalPosition.right = right
177
+ placement.rcNormalPosition.bottom = bottom
178
+ ctypes.windll.user32.SetWindowPlacement(hwnd, ctypes.byref(placement))
179
+ except Exception:
180
+ return
181
+
182
+
183
+ def apply_native_overlapped_style(window):
184
+ """Give the window real overlapped-window styles for snapping.
185
+
186
+ Qt's FramelessWindowHint produces WS_POPUP, which throws away the
187
+ shell behavior attached to the frame styles (snapping, taskbar
188
+ interaction, the move loop). Replace WS_POPUP with the standard
189
+ overlapped-window styles. The frame is never drawn because
190
+ WM_NCCALCSIZE collapses it.
191
+ """
192
+ if not _IS_WINDOWS:
193
+ return
194
+ hwnd = int(window.winId())
195
+ user32 = ctypes.windll.user32
196
+ style = user32.GetWindowLongW(hwnd, GWL_STYLE) & 0xFFFFFFFF
197
+ style = (style & ~WS_POPUP) | WS_OVERLAPPEDWINDOW
198
+ style &= 0xFFFFFFFF
199
+ # Win32 styles are signed 32-bit; convert before calling back.
200
+ if style >= 0x80000000:
201
+ style -= 0x100000000
202
+ user32.SetWindowLongW(hwnd, GWL_STYLE, style)
203
+ # Asking for a frame recalculation makes Windows re-query the frame
204
+ # (WM_NCCALCSIZE) so the collapsed client rect takes effect at once.
205
+ user32.SetWindowPos(
206
+ hwnd,
207
+ None,
208
+ 0,
209
+ 0,
210
+ 0,
211
+ 0,
212
+ SWP_FRAMECHANGED
213
+ | SWP_NOMOVE
214
+ | SWP_NOSIZE
215
+ | SWP_NOZORDER
216
+ | SWP_NOACTIVATE,
217
+ )
218
+
219
+
220
+ def current_styles(window):
221
+ """Return the window's live style and extended-style words, if any."""
222
+ if not _IS_WINDOWS:
223
+ return None, None
224
+ try:
225
+ hwnd = int(window.winId())
226
+ user32 = ctypes.windll.user32
227
+ style = user32.GetWindowLongW(hwnd, GWL_STYLE) & 0xFFFFFFFF
228
+ exstyle = user32.GetWindowLongW(hwnd, GWL_EXSTYLE) & 0xFFFFFFFF
229
+ return style, exstyle
230
+ except Exception:
231
+ return None, None
232
+
233
+
234
+ def handle_native_event(window, event_type, message):
235
+ """Route a native event to its handler; returns (handled, result)."""
236
+ if not _IS_WINDOWS or event_type != b"windows_generic_MSG":
237
+ return False, 0
238
+ try:
239
+ msg = wintypes.MSG.from_address(int(message))
240
+ except (TypeError, ValueError):
241
+ return False, 0
242
+ if msg.message == WM_NCHITTEST:
243
+ local = _local_point(window, msg.lParam)
244
+ code = window.hit_test_code(local)
245
+ _debug(f"NCHITTEST at {local.x()},{local.y()} -> HT{code}")
246
+ return True, code
247
+ if msg.message == WM_NCLBUTTONDBLCLK:
248
+ # The default is the system menu; we want maximize/restore like a
249
+ # real title bar.
250
+ if window.hit_test_code(_local_point(window, msg.lParam)) == HTCAPTION:
251
+ window.toggle_maximize()
252
+ return True, 0
253
+ if msg.message == WM_NCCALCSIZE:
254
+ # Collapse the non-client area so no native frame is drawn.
255
+ _collapse_frame(msg)
256
+ return True, 0
257
+ if msg.message == WM_GETMINMAXINFO:
258
+ _set_min_max_info(window, msg.lParam)
259
+ return True, 0
260
+ return False, 0
261
+
262
+
263
+ def _collapse_frame(msg):
264
+ """Make the client area fill the whole window (no native frame)."""
265
+ if not msg.lParam:
266
+ return
267
+ if msg.wParam:
268
+ params = NCCALCSIZE_PARAMS.from_address(int(msg.lParam))
269
+ window_rect = params.rgrc[1]
270
+ # The proposed client rect (rgrc[0]) becomes the window rect.
271
+ params.rgrc[0].left = window_rect.left
272
+ params.rgrc[0].top = window_rect.top
273
+ params.rgrc[0].right = window_rect.right
274
+ params.rgrc[0].bottom = window_rect.bottom
275
+
276
+
277
+ def _set_min_max_info(window, lparam):
278
+ """Enforce minimum size and a maximized size that fills the work area."""
279
+ if not lparam:
280
+ return
281
+ mmi = MINMAXINFO.from_address(int(lparam))
282
+ dpr = window.devicePixelRatioF()
283
+ mmi.ptMinTrackSize.x = int(window.minimumWidth() * dpr)
284
+ mmi.ptMinTrackSize.y = int(window.minimumHeight() * dpr)
285
+ # With the frame collapsed, a normal maximized window would overhang
286
+ # the screen by the frame width. Pin the maximized size to the work
287
+ # area so maximize and snap fill it exactly.
288
+ work = window.screen().availableGeometry()
289
+ mmi.ptMaxSize.x = int(work.width() * dpr)
290
+ mmi.ptMaxSize.y = int(work.height() * dpr)
291
+ mmi.ptMaxPosition.x = int(work.x() * dpr)
292
+ mmi.ptMaxPosition.y = int(work.y() * dpr)
@@ -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,15 @@
1
+ plotruler/__init__.py,sha256=m4YbXPapznnOV6dQ7Fvu3rYku1TPvViZrjhyt-QxXlk,86
2
+ plotruler/__main__.py,sha256=QZZ30UQ20lRlIy8olH2fKym-DJKMD8KkJ8TkPWU0Y_c,3238
3
+ plotruler/core.py,sha256=JrkxEFEwJIWiAPNbmxCOHDIu_ZiPT2lqYceU0I6hkdg,14548
4
+ plotruler/format.py,sha256=iIA2tun9WZCokTgtvhcPbjz_tywG-XoskN8YsK-GHIE,8688
5
+ plotruler/hotkey.py,sha256=9tlI8BkEbgsoP_ODojIHbk3TB4yhsFKxmWjrxXT-chA,8010
6
+ plotruler/overlay.py,sha256=JFRe1MWaLXyxebOoZhXXuZIxf8CN1kZMI6aTQgsSRxk,44465
7
+ plotruler/settings.py,sha256=9Q-OMCKJJgVf1Fay5_rea7PT3HdfLQaxn2MGe_sK-oI,4348
8
+ plotruler/storage.py,sha256=y35jXIMO826kEZVjNgkpHndEOWhJsk016sgxS6LsWok,4360
9
+ plotruler/titlebar.py,sha256=pOSPkM_SJI0taQ7gW_RwdLxqFLndhAEji-oFa7d6kLE,8720
10
+ plotruler/tray.py,sha256=y5IPHF_KiemFzwqLbs5kFzQnZ_FjyqbrvPAehpjVzYk,7104
11
+ plotruler/win_hittest.py,sha256=OnHUyi3iqWDrQfOYOHKacWe6NHzGmL1b1ktiMczb_kA,9756
12
+ plotruler-0.1.3.dist-info/METADATA,sha256=CfTMi8p1O9GBHaaI2C8yV4jeQn9rZ_eFBu_t7SQXZNU,8729
13
+ plotruler-0.1.3.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
14
+ plotruler-0.1.3.dist-info/licenses/LICENSE,sha256=9TX-edZF9gflRnx-Y46LPq4k4R1xasFwbGHLvQCTkHI,1066
15
+ plotruler-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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
+