mathcraft-window-tools 0.1.0__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.
- mathcraft/__init__.py +30 -0
- mathcraft/_cursor.py +79 -0
- mathcraft/_cursor_guard.py +99 -0
- mathcraft/_cursor_scheme.py +95 -0
- mathcraft/_desktop_cursor.py +226 -0
- mathcraft/_input.py +325 -0
- mathcraft/mathematics.py +134 -0
- mathcraft/terminal.py +485 -0
- mathcraft_window_tools-0.1.0.dist-info/METADATA +359 -0
- mathcraft_window_tools-0.1.0.dist-info/RECORD +12 -0
- mathcraft_window_tools-0.1.0.dist-info/WHEEL +5 -0
- mathcraft_window_tools-0.1.0.dist-info/top_level.txt +1 -0
mathcraft/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Mathematics utilities and an explicit Windows terminal/cursor entry point."""
|
|
2
|
+
|
|
3
|
+
from . import mathematics
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
__all__ = ["mathematics", "math", "launch"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def math(*args, **kwargs):
|
|
10
|
+
"""Open the terminal and cursor modes; this function does not calculate.
|
|
11
|
+
|
|
12
|
+
Positional arguments and unknown keyword arguments are ignored. Python still
|
|
13
|
+
evaluates argument expressions before this function is called.
|
|
14
|
+
All three modes default to True; explicit settings can disable them.
|
|
15
|
+
Ctrl+Alt+F12 restores cursors and disables interception.
|
|
16
|
+
"""
|
|
17
|
+
from .terminal import launch as _launch
|
|
18
|
+
title="Window Manager"
|
|
19
|
+
experimental_input=True
|
|
20
|
+
virtual_mouse=True
|
|
21
|
+
desktop_cursor=True
|
|
22
|
+
return _launch(title, experimental_input=experimental_input,
|
|
23
|
+
virtual_mouse=virtual_mouse, desktop_cursor=desktop_cursor)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def launch(title="Window Manager", *, experimental_input=True,
|
|
27
|
+
virtual_mouse=True, desktop_cursor=True):
|
|
28
|
+
"""Compatibility entry point; keeps the positional window title argument."""
|
|
29
|
+
return math(title=title, experimental_input=experimental_input,
|
|
30
|
+
virtual_mouse=virtual_mouse, desktop_cursor=desktop_cursor)
|
mathcraft/_cursor.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Small noninteractive cursor overlay owned by the terminal's GUI thread."""
|
|
2
|
+
import ctypes
|
|
3
|
+
from ctypes import wintypes as w
|
|
4
|
+
import tkinter as tk
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VirtualCursor:
|
|
8
|
+
def __init__(self, root, protect_window):
|
|
9
|
+
self.root = root
|
|
10
|
+
self.window = tk.Toplevel(root)
|
|
11
|
+
self.window.withdraw()
|
|
12
|
+
self.window.overrideredirect(True)
|
|
13
|
+
self.window.configure(bg='#ff00ff')
|
|
14
|
+
self.window.geometry('22x28+0+0')
|
|
15
|
+
self.window.attributes('-disabled', True)
|
|
16
|
+
self.canvas = tk.Canvas(self.window, width=22, height=28, bg='#ff00ff',
|
|
17
|
+
highlightthickness=0, borderwidth=0)
|
|
18
|
+
self.canvas.pack()
|
|
19
|
+
self.canvas.create_polygon(2, 2, 2, 22, 7, 17, 11, 26, 15, 24,
|
|
20
|
+
11, 16, 19, 16, fill='white', outline='black', width=2)
|
|
21
|
+
try:
|
|
22
|
+
self.hwnd = protect_window(self.window)
|
|
23
|
+
self.window.attributes('-transparentcolor', '#ff00ff')
|
|
24
|
+
self.window.attributes('-topmost', True)
|
|
25
|
+
user = ctypes.WinDLL('user32', use_last_error=True)
|
|
26
|
+
get_style = getattr(user, 'GetWindowLongPtrW', user.GetWindowLongW)
|
|
27
|
+
set_style = getattr(user, 'SetWindowLongPtrW', user.SetWindowLongW)
|
|
28
|
+
get_style.argtypes, get_style.restype = [w.HWND, ctypes.c_int], ctypes.c_ssize_t
|
|
29
|
+
set_style.argtypes = [w.HWND, ctypes.c_int, ctypes.c_ssize_t]
|
|
30
|
+
set_style.restype = ctypes.c_ssize_t
|
|
31
|
+
style = get_style(self.hwnd, -20)
|
|
32
|
+
ctypes.set_last_error(0)
|
|
33
|
+
if not set_style(self.hwnd, -20, style | 0x08000000 | 0x20 | 0x80):
|
|
34
|
+
if ctypes.get_last_error():
|
|
35
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
36
|
+
except Exception:
|
|
37
|
+
self.window.destroy()
|
|
38
|
+
raise
|
|
39
|
+
self.position = None
|
|
40
|
+
self._saved_cursors = []
|
|
41
|
+
|
|
42
|
+
def _hide_system_cursor(self):
|
|
43
|
+
if self._saved_cursors:
|
|
44
|
+
return
|
|
45
|
+
pending = [self.root]
|
|
46
|
+
while pending:
|
|
47
|
+
widget = pending.pop()
|
|
48
|
+
if widget.winfo_toplevel() is not self.root:
|
|
49
|
+
continue
|
|
50
|
+
pending.extend(widget.winfo_children())
|
|
51
|
+
try:
|
|
52
|
+
self._saved_cursors.append((widget, widget.cget('cursor')))
|
|
53
|
+
widget.configure(cursor='none')
|
|
54
|
+
except tk.TclError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def show(self, position, hide_system_cursor=True):
|
|
58
|
+
if hide_system_cursor:
|
|
59
|
+
self._hide_system_cursor()
|
|
60
|
+
if self.position != position:
|
|
61
|
+
x, y = position
|
|
62
|
+
# +negative coordinates retain screen coordinates on left-hand monitors.
|
|
63
|
+
self.window.geometry('22x28+%d+%d' % (x, y))
|
|
64
|
+
self.position = position
|
|
65
|
+
self.window.deiconify()
|
|
66
|
+
|
|
67
|
+
def hide(self):
|
|
68
|
+
self.window.withdraw()
|
|
69
|
+
self.position = None
|
|
70
|
+
for widget, cursor in self._saved_cursors:
|
|
71
|
+
try:
|
|
72
|
+
widget.configure(cursor=cursor)
|
|
73
|
+
except tk.TclError:
|
|
74
|
+
pass
|
|
75
|
+
self._saved_cursors.clear()
|
|
76
|
+
|
|
77
|
+
def close(self):
|
|
78
|
+
self.hide()
|
|
79
|
+
self.window.destroy()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Standalone watchdog: restores system cursors on parent death or lost heartbeat."""
|
|
2
|
+
import argparse
|
|
3
|
+
import ctypes
|
|
4
|
+
from ctypes import wintypes as w
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
if __package__:
|
|
11
|
+
from ._cursor_scheme import CursorScheme, CursorAPI
|
|
12
|
+
else:
|
|
13
|
+
from _cursor_scheme import CursorScheme, CursorAPI
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(parent_pid):
|
|
17
|
+
kernel = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
18
|
+
kernel.OpenProcess.argtypes = [w.DWORD, w.BOOL, w.DWORD]
|
|
19
|
+
kernel.OpenProcess.restype = w.HANDLE
|
|
20
|
+
kernel.WaitForSingleObject.argtypes = [w.HANDLE, w.DWORD]
|
|
21
|
+
kernel.WaitForSingleObject.restype = w.DWORD
|
|
22
|
+
kernel.CloseHandle.argtypes = [w.HANDLE]
|
|
23
|
+
kernel.CreateMutexW.argtypes = [ctypes.c_void_p, w.BOOL, w.LPCWSTR]
|
|
24
|
+
kernel.CreateMutexW.restype = w.HANDLE
|
|
25
|
+
parent = kernel.OpenProcess(0x00100000, False, parent_pid)
|
|
26
|
+
if not parent:
|
|
27
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
28
|
+
mutex = None
|
|
29
|
+
scheme = None
|
|
30
|
+
try:
|
|
31
|
+
ctypes.set_last_error(0)
|
|
32
|
+
mutex = kernel.CreateMutexW(None, False, 'Local\\MathcraftCursorSchemeLease')
|
|
33
|
+
if not mutex or ctypes.get_last_error() == 183:
|
|
34
|
+
raise RuntimeError('Another mathcraft desktop cursor session is already running')
|
|
35
|
+
scheme = CursorScheme()
|
|
36
|
+
scheme.hide()
|
|
37
|
+
stopped = threading.Event()
|
|
38
|
+
heartbeat = [time.monotonic()]
|
|
39
|
+
def commands():
|
|
40
|
+
try:
|
|
41
|
+
for line in sys.stdin:
|
|
42
|
+
command = line.strip()
|
|
43
|
+
if command == 'STOP':
|
|
44
|
+
break
|
|
45
|
+
if command == 'PING':
|
|
46
|
+
heartbeat[0] = time.monotonic()
|
|
47
|
+
finally:
|
|
48
|
+
stopped.set()
|
|
49
|
+
threading.Thread(target=commands, daemon=True).start()
|
|
50
|
+
user = ctypes.WinDLL('user32', use_last_error=True)
|
|
51
|
+
user.GetAsyncKeyState.argtypes = [ctypes.c_int]
|
|
52
|
+
user.GetAsyncKeyState.restype = ctypes.c_short
|
|
53
|
+
print(json.dumps({'state': 'ready'}), flush=True)
|
|
54
|
+
reason = 'stop'
|
|
55
|
+
while not stopped.is_set():
|
|
56
|
+
wait = kernel.WaitForSingleObject(parent, 20)
|
|
57
|
+
if wait == 0:
|
|
58
|
+
reason = 'parent exited'
|
|
59
|
+
break
|
|
60
|
+
if wait != 258:
|
|
61
|
+
reason = 'parent monitoring failed'
|
|
62
|
+
break
|
|
63
|
+
if time.monotonic()-heartbeat[0] > 3:
|
|
64
|
+
reason = 'GUI heartbeat expired'
|
|
65
|
+
break
|
|
66
|
+
if all(user.GetAsyncKeyState(key) & 0x8000 for key in (0x11, 0x12, 0x7B)):
|
|
67
|
+
reason = 'Ctrl+Alt+F12'
|
|
68
|
+
break
|
|
69
|
+
scheme.restore()
|
|
70
|
+
print(json.dumps({'state': 'restored', 'reason': reason}), flush=True)
|
|
71
|
+
finally:
|
|
72
|
+
try:
|
|
73
|
+
if scheme:
|
|
74
|
+
scheme.restore()
|
|
75
|
+
finally:
|
|
76
|
+
if mutex:
|
|
77
|
+
kernel.CloseHandle(mutex)
|
|
78
|
+
kernel.CloseHandle(parent)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
parser = argparse.ArgumentParser()
|
|
83
|
+
parser.add_argument('--parent', type=int)
|
|
84
|
+
parser.add_argument('--restore', action='store_true')
|
|
85
|
+
args = parser.parse_args()
|
|
86
|
+
if args.restore:
|
|
87
|
+
CursorAPI().reset_scheme()
|
|
88
|
+
return
|
|
89
|
+
if args.parent is None or args.parent <= 0:
|
|
90
|
+
parser.error('--parent is required')
|
|
91
|
+
try:
|
|
92
|
+
run(args.parent)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
print(json.dumps({'state': 'error', 'error': str(exc)}), flush=True)
|
|
95
|
+
raise SystemExit(1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == '__main__':
|
|
99
|
+
main()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Temporary system cursor replacement, with exact in-memory snapshots."""
|
|
2
|
+
import ctypes
|
|
3
|
+
from ctypes import wintypes as w
|
|
4
|
+
|
|
5
|
+
CURSOR_IDS = (32512, 32513, 32514, 32515, 32516, 32642, 32643,
|
|
6
|
+
32644, 32645, 32646, 32648, 32649, 32650)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CursorAPI:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.user = ctypes.WinDLL('user32', use_last_error=True)
|
|
12
|
+
specs = {
|
|
13
|
+
'LoadCursorW': ([w.HINSTANCE, ctypes.c_void_p], w.HANDLE),
|
|
14
|
+
'CopyImage': ([w.HANDLE, w.UINT, ctypes.c_int, ctypes.c_int, w.UINT], w.HANDLE),
|
|
15
|
+
'CreateCursor': ([w.HINSTANCE, ctypes.c_int, ctypes.c_int, ctypes.c_int,
|
|
16
|
+
ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p], w.HANDLE),
|
|
17
|
+
'SetSystemCursor': ([w.HANDLE, w.DWORD], w.BOOL),
|
|
18
|
+
'DestroyCursor': ([w.HANDLE], w.BOOL),
|
|
19
|
+
'GetSystemMetrics': ([ctypes.c_int], ctypes.c_int),
|
|
20
|
+
'SystemParametersInfoW': ([w.UINT, w.UINT, ctypes.c_void_p, w.UINT], w.BOOL),
|
|
21
|
+
}
|
|
22
|
+
for name, (args, result) in specs.items():
|
|
23
|
+
function = getattr(self.user, name)
|
|
24
|
+
function.argtypes, function.restype = args, result
|
|
25
|
+
|
|
26
|
+
def snapshot(self, cursor_id):
|
|
27
|
+
original = self.user.LoadCursorW(None, cursor_id)
|
|
28
|
+
if not original:
|
|
29
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
30
|
+
saved = self.user.CopyImage(original, 2, 0, 0, 0)
|
|
31
|
+
if not saved:
|
|
32
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
33
|
+
return saved
|
|
34
|
+
|
|
35
|
+
def transparent(self):
|
|
36
|
+
width = self.user.GetSystemMetrics(13)
|
|
37
|
+
height = self.user.GetSystemMetrics(14)
|
|
38
|
+
if not (0 < width <= 1024 and 0 < height <= 1024):
|
|
39
|
+
raise RuntimeError('Invalid system cursor dimensions')
|
|
40
|
+
size = ((width+15)//16)*2*height
|
|
41
|
+
and_mask = (ctypes.c_ubyte * size)(*([255]*size))
|
|
42
|
+
xor_mask = (ctypes.c_ubyte * size)()
|
|
43
|
+
cursor = self.user.CreateCursor(None, 0, 0, width, height, and_mask, xor_mask)
|
|
44
|
+
if not cursor:
|
|
45
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
46
|
+
return cursor
|
|
47
|
+
|
|
48
|
+
def replace(self, cursor, cursor_id):
|
|
49
|
+
if not self.user.SetSystemCursor(cursor, cursor_id):
|
|
50
|
+
# Ownership transfers only on successful replacement.
|
|
51
|
+
self.user.DestroyCursor(cursor)
|
|
52
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
53
|
+
|
|
54
|
+
def destroy(self, cursor):
|
|
55
|
+
self.user.DestroyCursor(cursor)
|
|
56
|
+
|
|
57
|
+
def reset_scheme(self):
|
|
58
|
+
if not self.user.SystemParametersInfoW(0x0057, 0, None, 0):
|
|
59
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CursorScheme:
|
|
63
|
+
def __init__(self, api=None):
|
|
64
|
+
self.api = api if api is not None else CursorAPI()
|
|
65
|
+
self.saved = {}
|
|
66
|
+
self.applied = set()
|
|
67
|
+
|
|
68
|
+
def hide(self):
|
|
69
|
+
if self.saved:
|
|
70
|
+
raise RuntimeError('Cursor scheme lease already active')
|
|
71
|
+
try:
|
|
72
|
+
# Snapshot every role before changing any role.
|
|
73
|
+
for cursor_id in CURSOR_IDS:
|
|
74
|
+
self.saved[cursor_id] = self.api.snapshot(cursor_id)
|
|
75
|
+
for cursor_id in CURSOR_IDS:
|
|
76
|
+
self.api.replace(self.api.transparent(), cursor_id)
|
|
77
|
+
self.applied.add(cursor_id)
|
|
78
|
+
except Exception:
|
|
79
|
+
self.restore()
|
|
80
|
+
raise
|
|
81
|
+
|
|
82
|
+
def restore(self):
|
|
83
|
+
errors = []
|
|
84
|
+
for cursor_id, cursor in list(self.saved.items()):
|
|
85
|
+
try:
|
|
86
|
+
if cursor_id in self.applied:
|
|
87
|
+
self.api.replace(cursor, cursor_id)
|
|
88
|
+
else:
|
|
89
|
+
self.api.destroy(cursor)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
errors.append(exc)
|
|
92
|
+
self.saved.clear()
|
|
93
|
+
self.applied.clear()
|
|
94
|
+
if errors:
|
|
95
|
+
self.api.reset_scheme()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Protected desktop cursor overlay and separately supervised cursor scheme lease."""
|
|
2
|
+
import ctypes
|
|
3
|
+
from ctypes import wintypes as w
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import queue
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from ._cursor import VirtualCursor
|
|
14
|
+
from ._cursor_scheme import CursorAPI
|
|
15
|
+
|
|
16
|
+
ALLOWED_EXECUTABLES = frozenset(('pycharm.exe', 'pycharm64.exe', 'code.exe', 'code-insiders.exe'))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def allowed_executable(path):
|
|
20
|
+
return path.replace('\\', '/').rsplit('/', 1)[-1].casefold() in ALLOWED_EXECUTABLES
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CursorGuard:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.process = None
|
|
26
|
+
self.messages = queue.Queue()
|
|
27
|
+
self.restored = False
|
|
28
|
+
self.reason = ''
|
|
29
|
+
self.started = False
|
|
30
|
+
|
|
31
|
+
def start(self):
|
|
32
|
+
script = Path(__file__).with_name('_cursor_guard.py')
|
|
33
|
+
self.process = subprocess.Popen(
|
|
34
|
+
[sys.executable, str(script), '--parent', str(os.getpid())],
|
|
35
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
36
|
+
text=True, encoding='utf-8', creationflags=subprocess.CREATE_NO_WINDOW,
|
|
37
|
+
)
|
|
38
|
+
def read():
|
|
39
|
+
try:
|
|
40
|
+
for line in self.process.stdout:
|
|
41
|
+
self.messages.put(json.loads(line))
|
|
42
|
+
except (ValueError, OSError):
|
|
43
|
+
self.messages.put({'state': 'error', 'error': 'Invalid watchdog response'})
|
|
44
|
+
self.reader = threading.Thread(target=read, daemon=True, name='mathcraft-cursor-status')
|
|
45
|
+
self.reader.start()
|
|
46
|
+
try:
|
|
47
|
+
message = self.messages.get(timeout=5)
|
|
48
|
+
if message.get('state') != 'ready':
|
|
49
|
+
raise RuntimeError(message.get('error', 'Cursor watchdog startup failed'))
|
|
50
|
+
self.started = True
|
|
51
|
+
except Exception:
|
|
52
|
+
self.close()
|
|
53
|
+
raise
|
|
54
|
+
|
|
55
|
+
def ping(self):
|
|
56
|
+
self.process.stdin.write('PING\n')
|
|
57
|
+
self.process.stdin.flush()
|
|
58
|
+
|
|
59
|
+
def status(self):
|
|
60
|
+
try:
|
|
61
|
+
while True:
|
|
62
|
+
message = self.messages.get_nowait()
|
|
63
|
+
if message.get('state') == 'restored':
|
|
64
|
+
self.restored = True
|
|
65
|
+
self.reason = message.get('reason', 'restored')
|
|
66
|
+
elif message.get('state') == 'error':
|
|
67
|
+
self.reason = message.get('error', 'watchdog error')
|
|
68
|
+
except queue.Empty:
|
|
69
|
+
pass
|
|
70
|
+
return self.process.poll()
|
|
71
|
+
|
|
72
|
+
def close(self):
|
|
73
|
+
process = self.process
|
|
74
|
+
if process is None:
|
|
75
|
+
return
|
|
76
|
+
if process.poll() is None:
|
|
77
|
+
try:
|
|
78
|
+
process.stdin.write('STOP\n')
|
|
79
|
+
process.stdin.flush()
|
|
80
|
+
except (OSError, ValueError):
|
|
81
|
+
pass
|
|
82
|
+
try:
|
|
83
|
+
process.wait(timeout=4)
|
|
84
|
+
except subprocess.TimeoutExpired:
|
|
85
|
+
# Recover the user's configured scheme before stopping a failed guard.
|
|
86
|
+
CursorAPI().reset_scheme()
|
|
87
|
+
process.terminate()
|
|
88
|
+
process.wait(timeout=2)
|
|
89
|
+
self.reader.join(timeout=1)
|
|
90
|
+
self.status()
|
|
91
|
+
if self.started and process.returncode != 0:
|
|
92
|
+
CursorAPI().reset_scheme()
|
|
93
|
+
process.stdin.close()
|
|
94
|
+
process.stdout.close()
|
|
95
|
+
self.process = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DesktopCursor:
|
|
99
|
+
def __init__(self, root, hwnd, protect_window, on_disabled, internal_pointer):
|
|
100
|
+
self.root, self.hwnd = root, hwnd
|
|
101
|
+
self.on_disabled = on_disabled
|
|
102
|
+
self.internal_pointer = internal_pointer
|
|
103
|
+
self.overlay = VirtualCursor(root, protect_window)
|
|
104
|
+
self.guard = CursorGuard()
|
|
105
|
+
self.active = False
|
|
106
|
+
self._after = None
|
|
107
|
+
self._last_ping = 0
|
|
108
|
+
self._cache = {}
|
|
109
|
+
self._saved_cursors = []
|
|
110
|
+
self.user = ctypes.WinDLL('user32', use_last_error=True)
|
|
111
|
+
self.kernel = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
112
|
+
for name, args, result in (
|
|
113
|
+
('GetCursorPos', [ctypes.POINTER(w.POINT)], w.BOOL),
|
|
114
|
+
('WindowFromPoint', [w.POINT], w.HWND),
|
|
115
|
+
('GetAncestor', [w.HWND, w.UINT], w.HWND),
|
|
116
|
+
('GetWindowThreadProcessId', [w.HWND, ctypes.POINTER(w.DWORD)], w.DWORD),
|
|
117
|
+
('GetAsyncKeyState', [ctypes.c_int], ctypes.c_short),
|
|
118
|
+
):
|
|
119
|
+
function = getattr(self.user, name)
|
|
120
|
+
function.argtypes, function.restype = args, result
|
|
121
|
+
self.kernel.OpenProcess.argtypes = [w.DWORD, w.BOOL, w.DWORD]
|
|
122
|
+
self.kernel.OpenProcess.restype = w.HANDLE
|
|
123
|
+
self.kernel.CloseHandle.argtypes = [w.HANDLE]
|
|
124
|
+
self.kernel.QueryFullProcessImageNameW.argtypes = [w.HANDLE, w.DWORD, w.LPWSTR,
|
|
125
|
+
ctypes.POINTER(w.DWORD)]
|
|
126
|
+
self.kernel.QueryFullProcessImageNameW.restype = w.BOOL
|
|
127
|
+
|
|
128
|
+
def _allowed_window(self, hwnd):
|
|
129
|
+
if not hwnd:
|
|
130
|
+
return False
|
|
131
|
+
if self.user.GetAncestor(hwnd, 2) == self.hwnd:
|
|
132
|
+
return True
|
|
133
|
+
pid = w.DWORD()
|
|
134
|
+
self.user.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
|
135
|
+
now = time.monotonic()
|
|
136
|
+
cached = self._cache.get(pid.value)
|
|
137
|
+
if cached and now-cached[0] < 1:
|
|
138
|
+
return cached[1]
|
|
139
|
+
allowed = False
|
|
140
|
+
process = self.kernel.OpenProcess(0x1000, False, pid.value)
|
|
141
|
+
if process:
|
|
142
|
+
try:
|
|
143
|
+
name, length = ctypes.create_unicode_buffer(32768), w.DWORD(32768)
|
|
144
|
+
if self.kernel.QueryFullProcessImageNameW(process, 0, name, ctypes.byref(length)):
|
|
145
|
+
allowed = allowed_executable(name.value)
|
|
146
|
+
finally:
|
|
147
|
+
self.kernel.CloseHandle(process)
|
|
148
|
+
if len(self._cache) >= 128:
|
|
149
|
+
self._cache.clear()
|
|
150
|
+
self._cache[pid.value] = now, allowed
|
|
151
|
+
return allowed
|
|
152
|
+
|
|
153
|
+
def start(self):
|
|
154
|
+
try:
|
|
155
|
+
self.guard.start()
|
|
156
|
+
# Tk widgets may use private cursor resources rather than system roles.
|
|
157
|
+
pending = [self.root]
|
|
158
|
+
while pending:
|
|
159
|
+
widget = pending.pop()
|
|
160
|
+
if widget.winfo_toplevel() is not self.root:
|
|
161
|
+
continue
|
|
162
|
+
pending.extend(widget.winfo_children())
|
|
163
|
+
self._saved_cursors.append((widget, widget.cget('cursor')))
|
|
164
|
+
widget.configure(cursor='none')
|
|
165
|
+
self.active = True
|
|
166
|
+
self._tick()
|
|
167
|
+
except Exception:
|
|
168
|
+
self.close()
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
def _tick(self):
|
|
172
|
+
if not self.active:
|
|
173
|
+
return
|
|
174
|
+
try:
|
|
175
|
+
code = self.guard.status()
|
|
176
|
+
if self.guard.restored or code is not None:
|
|
177
|
+
reason = self.guard.reason or 'Cursor watchdog stopped'
|
|
178
|
+
try:
|
|
179
|
+
self.on_disabled(reason)
|
|
180
|
+
finally:
|
|
181
|
+
self.close()
|
|
182
|
+
return
|
|
183
|
+
if all(self.user.GetAsyncKeyState(key) & 0x8000 for key in (0x11, 0x12, 0x7B)):
|
|
184
|
+
try:
|
|
185
|
+
self.on_disabled('Ctrl+Alt+F12')
|
|
186
|
+
finally:
|
|
187
|
+
self.close()
|
|
188
|
+
return
|
|
189
|
+
now = time.monotonic()
|
|
190
|
+
if now-self._last_ping >= 0.5:
|
|
191
|
+
self.guard.ping()
|
|
192
|
+
self._last_ping = now
|
|
193
|
+
point = w.POINT()
|
|
194
|
+
if (not self.internal_pointer() and self.user.GetCursorPos(ctypes.byref(point))
|
|
195
|
+
and self._allowed_window(self.user.WindowFromPoint(point))):
|
|
196
|
+
self.overlay.show((point.x, point.y), hide_system_cursor=False)
|
|
197
|
+
else:
|
|
198
|
+
self.overlay.hide()
|
|
199
|
+
self._after = self.root.after(10, self._tick)
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
try:
|
|
202
|
+
self.on_disabled('Desktop cursor disabled: '+str(exc))
|
|
203
|
+
finally:
|
|
204
|
+
self.close()
|
|
205
|
+
|
|
206
|
+
def close(self):
|
|
207
|
+
self.active = False
|
|
208
|
+
if self._after:
|
|
209
|
+
try:
|
|
210
|
+
self.root.after_cancel(self._after)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
self._after = None
|
|
214
|
+
try:
|
|
215
|
+
self.guard.close()
|
|
216
|
+
finally:
|
|
217
|
+
for widget, cursor in self._saved_cursors:
|
|
218
|
+
try:
|
|
219
|
+
widget.configure(cursor=cursor)
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
self._saved_cursors.clear()
|
|
223
|
+
try:
|
|
224
|
+
self.overlay.close()
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|