local-control 0.1.2__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.
- local_control/__init__.py +11 -0
- local_control/app.py +291 -0
- local_control/auth.py +240 -0
- local_control/cli.py +143 -0
- local_control/clipboard.py +342 -0
- local_control/config.py +47 -0
- local_control/control.py +1043 -0
- local_control/startup.py +140 -0
- local_control/static/css/styles.css +393 -0
- local_control/static/index.html +140 -0
- local_control/static/js/app.js +1658 -0
- local_control/utils/__init__.py +9 -0
- local_control/utils/qrcodegen.py +907 -0
- local_control/utils/terminal_qr.py +34 -0
- local_control-0.1.2.dist-info/METADATA +49 -0
- local_control-0.1.2.dist-info/RECORD +20 -0
- local_control-0.1.2.dist-info/WHEEL +5 -0
- local_control-0.1.2.dist-info/entry_points.txt +2 -0
- local_control-0.1.2.dist-info/licenses/LICENSE +21 -0
- local_control-0.1.2.dist-info/top_level.txt +1 -0
local_control/control.py
ADDED
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-platform input and power control utilities without external GUI dependencies.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import platform
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Dict, Optional, Sequence
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _BaseBackend:
|
|
15
|
+
def move_cursor(self, dx: float, dy: float) -> None: # pragma: no cover - OS specific
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
def click(self, button: str = "left", double: bool = False) -> None: # pragma: no cover
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
def button_action(self, button: str, action: str) -> None: # pragma: no cover
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
def scroll(self, vertical: float = 0.0, horizontal: float = 0.0) -> None: # pragma: no cover
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
def type_text(self, text: str) -> None: # pragma: no cover
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
def key_action(self, key: str, action: str) -> None: # pragma: no cover
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def state(self) -> Dict[str, float]:
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _WindowsBackend(_BaseBackend):
|
|
38
|
+
INPUT_MOUSE = 0
|
|
39
|
+
INPUT_KEYBOARD = 1
|
|
40
|
+
|
|
41
|
+
MOUSEEVENTF_LEFTDOWN = 0x0002
|
|
42
|
+
MOUSEEVENTF_LEFTUP = 0x0004
|
|
43
|
+
MOUSEEVENTF_RIGHTDOWN = 0x0008
|
|
44
|
+
MOUSEEVENTF_RIGHTUP = 0x0010
|
|
45
|
+
MOUSEEVENTF_MIDDLEDOWN = 0x0020
|
|
46
|
+
MOUSEEVENTF_MIDDLEUP = 0x0040
|
|
47
|
+
MOUSEEVENTF_WHEEL = 0x0800
|
|
48
|
+
MOUSEEVENTF_HWHEEL = 0x01000
|
|
49
|
+
|
|
50
|
+
KEYEVENTF_KEYUP = 0x0002
|
|
51
|
+
KEYEVENTF_UNICODE = 0x0004
|
|
52
|
+
|
|
53
|
+
_KEY_MAP: Dict[str, int] = {
|
|
54
|
+
"enter": 0x0D,
|
|
55
|
+
"esc": 0x1B,
|
|
56
|
+
"backspace": 0x08,
|
|
57
|
+
"tab": 0x09,
|
|
58
|
+
"up": 0x26,
|
|
59
|
+
"down": 0x28,
|
|
60
|
+
"left": 0x25,
|
|
61
|
+
"right": 0x27,
|
|
62
|
+
"delete": 0x2E,
|
|
63
|
+
"home": 0x24,
|
|
64
|
+
"end": 0x23,
|
|
65
|
+
"pageup": 0x21,
|
|
66
|
+
"pagedown": 0x22,
|
|
67
|
+
"shift": 0xA0,
|
|
68
|
+
"ctrl": 0xA2,
|
|
69
|
+
"alt": 0xA4,
|
|
70
|
+
"command": 0x5B, # Windows / Command key
|
|
71
|
+
"space": 0x20,
|
|
72
|
+
"minus": 0xBD,
|
|
73
|
+
"equals": 0xBB,
|
|
74
|
+
"leftbracket": 0xDB,
|
|
75
|
+
"rightbracket": 0xDD,
|
|
76
|
+
"backslash": 0xDC,
|
|
77
|
+
"semicolon": 0xBA,
|
|
78
|
+
"quote": 0xDE,
|
|
79
|
+
"comma": 0xBC,
|
|
80
|
+
"period": 0xBE,
|
|
81
|
+
"slash": 0xBF,
|
|
82
|
+
"grave": 0xC0,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_BUTTON_FLAGS = {
|
|
86
|
+
"left": (MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP),
|
|
87
|
+
"middle": (MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP),
|
|
88
|
+
"right": (MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def __init__(self) -> None:
|
|
92
|
+
from ctypes import wintypes
|
|
93
|
+
|
|
94
|
+
self._user32 = ctypes.WinDLL("user32", use_last_error=True)
|
|
95
|
+
|
|
96
|
+
self._user32.VkKeyScanW.argtypes = [wintypes.WCHAR]
|
|
97
|
+
self._user32.VkKeyScanW.restype = wintypes.SHORT
|
|
98
|
+
|
|
99
|
+
ULONG_PTR = wintypes.ULONG_PTR
|
|
100
|
+
|
|
101
|
+
class MOUSEINPUT(ctypes.Structure):
|
|
102
|
+
_fields_ = [
|
|
103
|
+
("dx", wintypes.LONG),
|
|
104
|
+
("dy", wintypes.LONG),
|
|
105
|
+
("mouseData", wintypes.DWORD),
|
|
106
|
+
("dwFlags", wintypes.DWORD),
|
|
107
|
+
("time", wintypes.DWORD),
|
|
108
|
+
("dwExtraInfo", ULONG_PTR),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
class KEYBDINPUT(ctypes.Structure):
|
|
112
|
+
_fields_ = [
|
|
113
|
+
("wVk", wintypes.WORD),
|
|
114
|
+
("wScan", wintypes.WORD),
|
|
115
|
+
("dwFlags", wintypes.DWORD),
|
|
116
|
+
("time", wintypes.DWORD),
|
|
117
|
+
("dwExtraInfo", ULONG_PTR),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
class HARDWAREINPUT(ctypes.Structure):
|
|
121
|
+
_fields_ = [
|
|
122
|
+
("uMsg", wintypes.DWORD),
|
|
123
|
+
("wParamL", wintypes.WORD),
|
|
124
|
+
("wParamH", wintypes.WORD),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
class _INPUT_UNION(ctypes.Union):
|
|
128
|
+
_fields_ = [
|
|
129
|
+
("mi", MOUSEINPUT),
|
|
130
|
+
("ki", KEYBDINPUT),
|
|
131
|
+
("hi", HARDWAREINPUT),
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
class INPUT(ctypes.Structure):
|
|
135
|
+
_anonymous_ = ("value",)
|
|
136
|
+
_fields_ = [("type", wintypes.DWORD), ("value", _INPUT_UNION)]
|
|
137
|
+
|
|
138
|
+
self._wintypes = wintypes
|
|
139
|
+
self._MOUSEINPUT = MOUSEINPUT
|
|
140
|
+
self._KEYBDINPUT = KEYBDINPUT
|
|
141
|
+
self._INPUT = INPUT
|
|
142
|
+
|
|
143
|
+
def _send_inputs(self, inputs: Sequence[ctypes.Structure]) -> None:
|
|
144
|
+
if not inputs:
|
|
145
|
+
return
|
|
146
|
+
array_type = self._INPUT * len(inputs)
|
|
147
|
+
array = array_type(*inputs)
|
|
148
|
+
sent = self._user32.SendInput(
|
|
149
|
+
len(array),
|
|
150
|
+
ctypes.byref(array),
|
|
151
|
+
ctypes.sizeof(self._INPUT),
|
|
152
|
+
)
|
|
153
|
+
if sent != len(array):
|
|
154
|
+
raise OSError(ctypes.get_last_error())
|
|
155
|
+
|
|
156
|
+
def move_cursor(self, dx: float, dy: float) -> None:
|
|
157
|
+
point = self._wintypes.POINT()
|
|
158
|
+
if not self._user32.GetCursorPos(ctypes.byref(point)):
|
|
159
|
+
raise OSError(ctypes.get_last_error())
|
|
160
|
+
target_x = int(round(point.x + dx))
|
|
161
|
+
target_y = int(round(point.y + dy))
|
|
162
|
+
if not self._user32.SetCursorPos(target_x, target_y):
|
|
163
|
+
raise OSError(ctypes.get_last_error())
|
|
164
|
+
|
|
165
|
+
def click(self, button: str = "left", double: bool = False) -> None:
|
|
166
|
+
if button not in {"left", "middle", "right"}:
|
|
167
|
+
raise ValueError(f"Unsupported button: {button}")
|
|
168
|
+
down_flag, up_flag = self._BUTTON_FLAGS[button]
|
|
169
|
+
|
|
170
|
+
events = []
|
|
171
|
+
for _ in range(2 if double else 1):
|
|
172
|
+
down = self._INPUT()
|
|
173
|
+
down.type = self.INPUT_MOUSE
|
|
174
|
+
down.value.mi = self._MOUSEINPUT(
|
|
175
|
+
dx=0,
|
|
176
|
+
dy=0,
|
|
177
|
+
mouseData=0,
|
|
178
|
+
dwFlags=down_flag,
|
|
179
|
+
time=0,
|
|
180
|
+
dwExtraInfo=0,
|
|
181
|
+
)
|
|
182
|
+
up = self._INPUT()
|
|
183
|
+
up.type = self.INPUT_MOUSE
|
|
184
|
+
up.value.mi = self._MOUSEINPUT(
|
|
185
|
+
dx=0,
|
|
186
|
+
dy=0,
|
|
187
|
+
mouseData=0,
|
|
188
|
+
dwFlags=up_flag,
|
|
189
|
+
time=0,
|
|
190
|
+
dwExtraInfo=0,
|
|
191
|
+
)
|
|
192
|
+
events.extend([down, up])
|
|
193
|
+
self._send_inputs(events)
|
|
194
|
+
|
|
195
|
+
def button_action(self, button: str, action: str) -> None:
|
|
196
|
+
if button not in self._BUTTON_FLAGS:
|
|
197
|
+
raise ValueError(f"Unsupported button: {button}")
|
|
198
|
+
if action not in {"down", "up"}:
|
|
199
|
+
raise ValueError(f"Unsupported button action: {action}")
|
|
200
|
+
down_flag, up_flag = self._BUTTON_FLAGS[button]
|
|
201
|
+
flag = down_flag if action == "down" else up_flag
|
|
202
|
+
event = self._INPUT()
|
|
203
|
+
event.type = self.INPUT_MOUSE
|
|
204
|
+
event.value.mi = self._MOUSEINPUT(
|
|
205
|
+
dx=0,
|
|
206
|
+
dy=0,
|
|
207
|
+
mouseData=0,
|
|
208
|
+
dwFlags=flag,
|
|
209
|
+
time=0,
|
|
210
|
+
dwExtraInfo=0,
|
|
211
|
+
)
|
|
212
|
+
self._send_inputs([event])
|
|
213
|
+
|
|
214
|
+
def scroll(self, vertical: float = 0.0, horizontal: float = 0.0) -> None:
|
|
215
|
+
if vertical:
|
|
216
|
+
value = -vertical
|
|
217
|
+
amount = int(round(value * 120))
|
|
218
|
+
if amount == 0:
|
|
219
|
+
amount = -120 if vertical > 0 else 120
|
|
220
|
+
event = self._INPUT()
|
|
221
|
+
event.type = self.INPUT_MOUSE
|
|
222
|
+
event.value.mi = self._MOUSEINPUT(
|
|
223
|
+
dx=0,
|
|
224
|
+
dy=0,
|
|
225
|
+
mouseData=amount,
|
|
226
|
+
dwFlags=self.MOUSEEVENTF_WHEEL,
|
|
227
|
+
time=0,
|
|
228
|
+
dwExtraInfo=0,
|
|
229
|
+
)
|
|
230
|
+
self._send_inputs([event])
|
|
231
|
+
if horizontal:
|
|
232
|
+
amount = int(round(horizontal * 120))
|
|
233
|
+
if amount == 0:
|
|
234
|
+
amount = 120 if horizontal > 0 else -120
|
|
235
|
+
event = self._INPUT()
|
|
236
|
+
event.type = self.INPUT_MOUSE
|
|
237
|
+
event.value.mi = self._MOUSEINPUT(
|
|
238
|
+
dx=0,
|
|
239
|
+
dy=0,
|
|
240
|
+
mouseData=amount,
|
|
241
|
+
dwFlags=self.MOUSEEVENTF_HWHEEL,
|
|
242
|
+
time=0,
|
|
243
|
+
dwExtraInfo=0,
|
|
244
|
+
)
|
|
245
|
+
self._send_inputs([event])
|
|
246
|
+
|
|
247
|
+
def type_text(self, text: str) -> None:
|
|
248
|
+
if not text:
|
|
249
|
+
return
|
|
250
|
+
events = []
|
|
251
|
+
for char in text:
|
|
252
|
+
code = ord(char)
|
|
253
|
+
down = self._INPUT()
|
|
254
|
+
down.type = self.INPUT_KEYBOARD
|
|
255
|
+
down.value.ki = self._KEYBDINPUT(
|
|
256
|
+
wVk=0,
|
|
257
|
+
wScan=code,
|
|
258
|
+
dwFlags=self.KEYEVENTF_UNICODE,
|
|
259
|
+
time=0,
|
|
260
|
+
dwExtraInfo=0,
|
|
261
|
+
)
|
|
262
|
+
up = self._INPUT()
|
|
263
|
+
up.type = self.INPUT_KEYBOARD
|
|
264
|
+
up.value.ki = self._KEYBDINPUT(
|
|
265
|
+
wVk=0,
|
|
266
|
+
wScan=code,
|
|
267
|
+
dwFlags=self.KEYEVENTF_UNICODE | self.KEYEVENTF_KEYUP,
|
|
268
|
+
time=0,
|
|
269
|
+
dwExtraInfo=0,
|
|
270
|
+
)
|
|
271
|
+
events.extend([down, up])
|
|
272
|
+
self._send_inputs(events)
|
|
273
|
+
|
|
274
|
+
def key_action(self, key: str, action: str) -> None:
|
|
275
|
+
vk = self._KEY_MAP.get(key)
|
|
276
|
+
if vk is None and len(key) == 1:
|
|
277
|
+
char = key
|
|
278
|
+
result = self._user32.VkKeyScanW(ctypes.c_wchar(char))
|
|
279
|
+
if result != -1:
|
|
280
|
+
vk = result & 0xFF
|
|
281
|
+
if vk is None:
|
|
282
|
+
raise ValueError(f"Unsupported key: {key}")
|
|
283
|
+
if action not in {"press", "down", "up"}:
|
|
284
|
+
raise ValueError(f"Unsupported action: {action}")
|
|
285
|
+
|
|
286
|
+
event_down = self._INPUT()
|
|
287
|
+
event_down.type = self.INPUT_KEYBOARD
|
|
288
|
+
event_down.value.ki = self._KEYBDINPUT(
|
|
289
|
+
wVk=vk,
|
|
290
|
+
wScan=0,
|
|
291
|
+
dwFlags=0,
|
|
292
|
+
time=0,
|
|
293
|
+
dwExtraInfo=0,
|
|
294
|
+
)
|
|
295
|
+
event_up = self._INPUT()
|
|
296
|
+
event_up.type = self.INPUT_KEYBOARD
|
|
297
|
+
event_up.value.ki = self._KEYBDINPUT(
|
|
298
|
+
wVk=vk,
|
|
299
|
+
wScan=0,
|
|
300
|
+
dwFlags=self.KEYEVENTF_KEYUP,
|
|
301
|
+
time=0,
|
|
302
|
+
dwExtraInfo=0,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if action == "press":
|
|
306
|
+
self._send_inputs([event_down, event_up])
|
|
307
|
+
elif action == "down":
|
|
308
|
+
self._send_inputs([event_down])
|
|
309
|
+
else:
|
|
310
|
+
self._send_inputs([event_up])
|
|
311
|
+
|
|
312
|
+
def state(self) -> Dict[str, float]:
|
|
313
|
+
point = self._wintypes.POINT()
|
|
314
|
+
if not self._user32.GetCursorPos(ctypes.byref(point)):
|
|
315
|
+
raise OSError(ctypes.get_last_error())
|
|
316
|
+
width = self._user32.GetSystemMetrics(0)
|
|
317
|
+
height = self._user32.GetSystemMetrics(1)
|
|
318
|
+
return {
|
|
319
|
+
"x": float(point.x),
|
|
320
|
+
"y": float(point.y),
|
|
321
|
+
"width": float(width),
|
|
322
|
+
"height": float(height),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class _DarwinBackend(_BaseBackend):
|
|
327
|
+
kCGHIDEventTap = 0
|
|
328
|
+
|
|
329
|
+
kCGEventLeftMouseDown = 1
|
|
330
|
+
kCGEventLeftMouseUp = 2
|
|
331
|
+
kCGEventRightMouseDown = 3
|
|
332
|
+
kCGEventRightMouseUp = 4
|
|
333
|
+
kCGEventOtherMouseDown = 25
|
|
334
|
+
kCGEventOtherMouseUp = 26
|
|
335
|
+
kCGEventMouseMoved = 5
|
|
336
|
+
kCGEventScrollWheel = 22
|
|
337
|
+
|
|
338
|
+
kCGMouseButtonLeft = 0
|
|
339
|
+
kCGMouseButtonRight = 1
|
|
340
|
+
kCGMouseButtonCenter = 2
|
|
341
|
+
|
|
342
|
+
kCGScrollEventUnitPixel = 0
|
|
343
|
+
kCGScrollEventUnitLine = 1
|
|
344
|
+
|
|
345
|
+
_KEY_MAP: Dict[str, int] = {
|
|
346
|
+
"enter": 36,
|
|
347
|
+
"esc": 53,
|
|
348
|
+
"backspace": 51,
|
|
349
|
+
"tab": 48,
|
|
350
|
+
"up": 126,
|
|
351
|
+
"down": 125,
|
|
352
|
+
"left": 123,
|
|
353
|
+
"right": 124,
|
|
354
|
+
"delete": 117,
|
|
355
|
+
"home": 115,
|
|
356
|
+
"end": 119,
|
|
357
|
+
"pageup": 116,
|
|
358
|
+
"pagedown": 121,
|
|
359
|
+
"shift": 56,
|
|
360
|
+
"ctrl": 59,
|
|
361
|
+
"alt": 58,
|
|
362
|
+
"command": 55,
|
|
363
|
+
"space": 49,
|
|
364
|
+
"grave": 50,
|
|
365
|
+
"minus": 27,
|
|
366
|
+
"equals": 24,
|
|
367
|
+
"leftbracket": 33,
|
|
368
|
+
"rightbracket": 30,
|
|
369
|
+
"backslash": 42,
|
|
370
|
+
"semicolon": 41,
|
|
371
|
+
"quote": 39,
|
|
372
|
+
"comma": 43,
|
|
373
|
+
"period": 47,
|
|
374
|
+
"slash": 44,
|
|
375
|
+
"0": 29,
|
|
376
|
+
"1": 18,
|
|
377
|
+
"2": 19,
|
|
378
|
+
"3": 20,
|
|
379
|
+
"4": 21,
|
|
380
|
+
"5": 23,
|
|
381
|
+
"6": 22,
|
|
382
|
+
"7": 26,
|
|
383
|
+
"8": 28,
|
|
384
|
+
"9": 25,
|
|
385
|
+
"a": 0,
|
|
386
|
+
"b": 11,
|
|
387
|
+
"c": 8,
|
|
388
|
+
"d": 2,
|
|
389
|
+
"e": 14,
|
|
390
|
+
"f": 3,
|
|
391
|
+
"g": 5,
|
|
392
|
+
"h": 4,
|
|
393
|
+
"i": 34,
|
|
394
|
+
"j": 38,
|
|
395
|
+
"k": 40,
|
|
396
|
+
"l": 37,
|
|
397
|
+
"m": 46,
|
|
398
|
+
"n": 45,
|
|
399
|
+
"o": 31,
|
|
400
|
+
"p": 35,
|
|
401
|
+
"q": 12,
|
|
402
|
+
"r": 15,
|
|
403
|
+
"s": 1,
|
|
404
|
+
"t": 17,
|
|
405
|
+
"u": 32,
|
|
406
|
+
"v": 9,
|
|
407
|
+
"w": 13,
|
|
408
|
+
"x": 7,
|
|
409
|
+
"y": 16,
|
|
410
|
+
"z": 6,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
def __init__(self) -> None:
|
|
414
|
+
from ctypes import (
|
|
415
|
+
c_bool,
|
|
416
|
+
c_double,
|
|
417
|
+
c_int32,
|
|
418
|
+
c_size_t,
|
|
419
|
+
c_uint16,
|
|
420
|
+
c_uint32,
|
|
421
|
+
c_uint64,
|
|
422
|
+
c_void_p,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
self._c_bool = c_bool
|
|
426
|
+
self._c_double = c_double
|
|
427
|
+
self._c_int32 = c_int32
|
|
428
|
+
self._c_uint16 = c_uint16
|
|
429
|
+
self._c_uint32 = c_uint32
|
|
430
|
+
self._c_uint64 = c_uint64
|
|
431
|
+
self._c_void_p = c_void_p
|
|
432
|
+
self._c_size_t = c_size_t
|
|
433
|
+
|
|
434
|
+
self._quartz = ctypes.CDLL(
|
|
435
|
+
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
class CGPoint(ctypes.Structure):
|
|
439
|
+
_fields_ = [("x", c_double), ("y", c_double)]
|
|
440
|
+
|
|
441
|
+
self.CGPoint = CGPoint
|
|
442
|
+
|
|
443
|
+
self._quartz.CGEventCreateMouseEvent.restype = c_void_p
|
|
444
|
+
self._quartz.CGEventCreateMouseEvent.argtypes = [
|
|
445
|
+
c_void_p,
|
|
446
|
+
c_uint32,
|
|
447
|
+
CGPoint,
|
|
448
|
+
c_uint32,
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
self._quartz.CGEventCreateScrollWheelEvent.restype = c_void_p
|
|
452
|
+
self._quartz.CGEventCreateScrollWheelEvent.argtypes = [
|
|
453
|
+
c_void_p,
|
|
454
|
+
c_uint32,
|
|
455
|
+
c_uint32,
|
|
456
|
+
c_int32,
|
|
457
|
+
c_int32,
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
self._quartz.CGEventCreateKeyboardEvent.restype = c_void_p
|
|
461
|
+
self._quartz.CGEventCreateKeyboardEvent.argtypes = [
|
|
462
|
+
c_void_p,
|
|
463
|
+
c_uint16,
|
|
464
|
+
c_bool,
|
|
465
|
+
]
|
|
466
|
+
|
|
467
|
+
self._quartz.CGEventKeyboardSetUnicodeString.argtypes = [
|
|
468
|
+
c_void_p,
|
|
469
|
+
ctypes.c_long,
|
|
470
|
+
ctypes.POINTER(c_uint16),
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
self._quartz.CGEventPost.argtypes = [c_uint32, c_void_p]
|
|
474
|
+
self._quartz.CFRelease.argtypes = [c_void_p]
|
|
475
|
+
|
|
476
|
+
self._quartz.CGEventCreate.restype = c_void_p
|
|
477
|
+
self._quartz.CGEventCreate.argtypes = [c_void_p]
|
|
478
|
+
self._quartz.CGEventGetLocation.argtypes = [c_void_p]
|
|
479
|
+
self._quartz.CGEventGetLocation.restype = CGPoint
|
|
480
|
+
self._quartz.CGMainDisplayID.restype = c_uint32
|
|
481
|
+
self._quartz.CGDisplayPixelsWide.argtypes = [c_uint32]
|
|
482
|
+
self._quartz.CGDisplayPixelsWide.restype = c_size_t
|
|
483
|
+
self._quartz.CGDisplayPixelsHigh.argtypes = [c_uint32]
|
|
484
|
+
self._quartz.CGDisplayPixelsHigh.restype = c_size_t
|
|
485
|
+
self._quartz.CGEventSetFlags.argtypes = [c_void_p, c_uint64]
|
|
486
|
+
self._quartz.CGEventSetFlags.restype = None
|
|
487
|
+
|
|
488
|
+
def _prepare_event(self, event: ctypes.c_void_p, flags: int = 0) -> ctypes.c_void_p:
|
|
489
|
+
if event:
|
|
490
|
+
self._quartz.CGEventSetFlags(event, self._c_uint64(flags))
|
|
491
|
+
return event
|
|
492
|
+
|
|
493
|
+
def _current_position(self) -> "CGPoint":
|
|
494
|
+
event = self._quartz.CGEventCreate(self._c_void_p())
|
|
495
|
+
if not event:
|
|
496
|
+
raise RuntimeError("Failed to create CGEvent for cursor location.")
|
|
497
|
+
try:
|
|
498
|
+
return self._quartz.CGEventGetLocation(event)
|
|
499
|
+
finally:
|
|
500
|
+
self._quartz.CFRelease(event)
|
|
501
|
+
|
|
502
|
+
def _post_event(self, event: ctypes.c_void_p) -> None:
|
|
503
|
+
if not event:
|
|
504
|
+
raise RuntimeError("Unable to create Quartz event.")
|
|
505
|
+
try:
|
|
506
|
+
self._quartz.CGEventPost(self.kCGHIDEventTap, event)
|
|
507
|
+
finally:
|
|
508
|
+
self._quartz.CFRelease(event)
|
|
509
|
+
|
|
510
|
+
def move_cursor(self, dx: float, dy: float) -> None:
|
|
511
|
+
current = self._current_position()
|
|
512
|
+
target = self.CGPoint(current.x + dx, current.y + dy)
|
|
513
|
+
event = self._prepare_event(
|
|
514
|
+
self._quartz.CGEventCreateMouseEvent(
|
|
515
|
+
None,
|
|
516
|
+
self.kCGEventMouseMoved,
|
|
517
|
+
target,
|
|
518
|
+
self.kCGMouseButtonLeft,
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
self._post_event(event)
|
|
522
|
+
|
|
523
|
+
def click(self, button: str = "left", double: bool = False) -> None:
|
|
524
|
+
if button not in {"left", "middle", "right"}:
|
|
525
|
+
raise ValueError(f"Unsupported button: {button}")
|
|
526
|
+
if button == "left":
|
|
527
|
+
down_type = self.kCGEventLeftMouseDown
|
|
528
|
+
up_type = self.kCGEventLeftMouseUp
|
|
529
|
+
cg_button = self.kCGMouseButtonLeft
|
|
530
|
+
elif button == "right":
|
|
531
|
+
down_type = self.kCGEventRightMouseDown
|
|
532
|
+
up_type = self.kCGEventRightMouseUp
|
|
533
|
+
cg_button = self.kCGMouseButtonRight
|
|
534
|
+
else:
|
|
535
|
+
down_type = self.kCGEventOtherMouseDown
|
|
536
|
+
up_type = self.kCGEventOtherMouseUp
|
|
537
|
+
cg_button = self.kCGMouseButtonCenter
|
|
538
|
+
|
|
539
|
+
current = self._current_position()
|
|
540
|
+
point = self.CGPoint(current.x, current.y)
|
|
541
|
+
|
|
542
|
+
for _ in range(2 if double else 1):
|
|
543
|
+
down_event = self._prepare_event(
|
|
544
|
+
self._quartz.CGEventCreateMouseEvent(None, down_type, point, cg_button)
|
|
545
|
+
)
|
|
546
|
+
up_event = self._prepare_event(
|
|
547
|
+
self._quartz.CGEventCreateMouseEvent(None, up_type, point, cg_button)
|
|
548
|
+
)
|
|
549
|
+
self._post_event(down_event)
|
|
550
|
+
self._post_event(up_event)
|
|
551
|
+
|
|
552
|
+
def button_action(self, button: str, action: str) -> None:
|
|
553
|
+
if button not in {"left", "middle", "right"}:
|
|
554
|
+
raise ValueError(f"Unsupported button: {button}")
|
|
555
|
+
if action not in {"down", "up"}:
|
|
556
|
+
raise ValueError(f"Unsupported button action: {action}")
|
|
557
|
+
if button == "left":
|
|
558
|
+
down_type = self.kCGEventLeftMouseDown
|
|
559
|
+
up_type = self.kCGEventLeftMouseUp
|
|
560
|
+
cg_button = self.kCGMouseButtonLeft
|
|
561
|
+
elif button == "right":
|
|
562
|
+
down_type = self.kCGEventRightMouseDown
|
|
563
|
+
up_type = self.kCGEventRightMouseUp
|
|
564
|
+
cg_button = self.kCGMouseButtonRight
|
|
565
|
+
else:
|
|
566
|
+
down_type = self.kCGEventOtherMouseDown
|
|
567
|
+
up_type = self.kCGEventOtherMouseUp
|
|
568
|
+
cg_button = self.kCGMouseButtonCenter
|
|
569
|
+
event_type = down_type if action == "down" else up_type
|
|
570
|
+
current = self._current_position()
|
|
571
|
+
point = self.CGPoint(current.x, current.y)
|
|
572
|
+
event = self._prepare_event(
|
|
573
|
+
self._quartz.CGEventCreateMouseEvent(None, event_type, point, cg_button)
|
|
574
|
+
)
|
|
575
|
+
self._post_event(event)
|
|
576
|
+
|
|
577
|
+
def scroll(self, vertical: float = 0.0, horizontal: float = 0.0) -> None:
|
|
578
|
+
if not vertical and not horizontal:
|
|
579
|
+
return
|
|
580
|
+
|
|
581
|
+
def _normalize(value: float, invert: bool = False) -> int:
|
|
582
|
+
"""
|
|
583
|
+
Convert browser wheel deltas (typically ~120 per detent) into smaller
|
|
584
|
+
macOS-friendly increments. The divisor keeps physical scroll wheels from
|
|
585
|
+
jumping entire pages while still allowing smooth trackpad gestures.
|
|
586
|
+
"""
|
|
587
|
+
if value == 0.0:
|
|
588
|
+
return 0
|
|
589
|
+
divisor = 40.0
|
|
590
|
+
scaled = value / divisor
|
|
591
|
+
if invert:
|
|
592
|
+
scaled = -scaled
|
|
593
|
+
rounded = int(round(scaled))
|
|
594
|
+
if rounded == 0:
|
|
595
|
+
rounded = -1 if scaled < 0 else 1
|
|
596
|
+
return rounded
|
|
597
|
+
|
|
598
|
+
vert = _normalize(vertical, invert=True)
|
|
599
|
+
horiz = _normalize(horizontal)
|
|
600
|
+
event = self._prepare_event(
|
|
601
|
+
self._quartz.CGEventCreateScrollWheelEvent(
|
|
602
|
+
None,
|
|
603
|
+
self.kCGScrollEventUnitLine,
|
|
604
|
+
2,
|
|
605
|
+
vert,
|
|
606
|
+
horiz,
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
self._post_event(event)
|
|
610
|
+
|
|
611
|
+
def type_text(self, text: str) -> None:
|
|
612
|
+
if not text:
|
|
613
|
+
return
|
|
614
|
+
utf16 = text.encode("utf-16-le")
|
|
615
|
+
units = [int.from_bytes(utf16[i : i + 2], "little") for i in range(0, len(utf16), 2)]
|
|
616
|
+
buffer_type = self._c_uint16 * len(units)
|
|
617
|
+
buf = buffer_type(*units)
|
|
618
|
+
down = self._prepare_event(self._quartz.CGEventCreateKeyboardEvent(None, 0, True))
|
|
619
|
+
self._quartz.CGEventKeyboardSetUnicodeString(down, len(units), buf)
|
|
620
|
+
up = self._prepare_event(self._quartz.CGEventCreateKeyboardEvent(None, 0, False))
|
|
621
|
+
self._quartz.CGEventKeyboardSetUnicodeString(up, len(units), buf)
|
|
622
|
+
self._post_event(down)
|
|
623
|
+
self._post_event(up)
|
|
624
|
+
|
|
625
|
+
def key_action(self, key: str, action: str) -> None:
|
|
626
|
+
keycode = self._KEY_MAP.get(key)
|
|
627
|
+
if keycode is None:
|
|
628
|
+
raise ValueError(f"Unsupported key: {key}")
|
|
629
|
+
if action not in {"press", "down", "up"}:
|
|
630
|
+
raise ValueError(f"Unsupported action: {action}")
|
|
631
|
+
if action in {"press", "down"}:
|
|
632
|
+
down = self._prepare_event(self._quartz.CGEventCreateKeyboardEvent(None, keycode, True))
|
|
633
|
+
self._post_event(down)
|
|
634
|
+
if action in {"press", "up"}:
|
|
635
|
+
up = self._prepare_event(self._quartz.CGEventCreateKeyboardEvent(None, keycode, False))
|
|
636
|
+
self._post_event(up)
|
|
637
|
+
|
|
638
|
+
def state(self) -> Dict[str, float]:
|
|
639
|
+
current = self._current_position()
|
|
640
|
+
display = self._quartz.CGMainDisplayID()
|
|
641
|
+
width = float(self._quartz.CGDisplayPixelsWide(display))
|
|
642
|
+
height = float(self._quartz.CGDisplayPixelsHigh(display))
|
|
643
|
+
return {
|
|
644
|
+
"x": float(current.x),
|
|
645
|
+
"y": float(current.y),
|
|
646
|
+
"width": width,
|
|
647
|
+
"height": height,
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class _X11Backend(_BaseBackend):
|
|
652
|
+
_SHIFT_REQUIRED = set("~!@#$%^&*()_+{}|:\"<>?ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
653
|
+
|
|
654
|
+
_KEY_MAP: Dict[str, str] = {
|
|
655
|
+
"enter": "Return",
|
|
656
|
+
"esc": "Escape",
|
|
657
|
+
"backspace": "BackSpace",
|
|
658
|
+
"tab": "Tab",
|
|
659
|
+
"up": "Up",
|
|
660
|
+
"down": "Down",
|
|
661
|
+
"left": "Left",
|
|
662
|
+
"right": "Right",
|
|
663
|
+
"delete": "Delete",
|
|
664
|
+
"home": "Home",
|
|
665
|
+
"end": "End",
|
|
666
|
+
"pageup": "Page_Up",
|
|
667
|
+
"pagedown": "Page_Down",
|
|
668
|
+
"shift": "Shift_L",
|
|
669
|
+
"ctrl": "Control_L",
|
|
670
|
+
"alt": "Alt_L",
|
|
671
|
+
"command": "Super_L",
|
|
672
|
+
"space": "space",
|
|
673
|
+
"minus": "minus",
|
|
674
|
+
"equals": "equal",
|
|
675
|
+
"leftbracket": "bracketleft",
|
|
676
|
+
"rightbracket": "bracketright",
|
|
677
|
+
"backslash": "backslash",
|
|
678
|
+
"semicolon": "semicolon",
|
|
679
|
+
"quote": "apostrophe",
|
|
680
|
+
"comma": "comma",
|
|
681
|
+
"period": "period",
|
|
682
|
+
"slash": "slash",
|
|
683
|
+
"grave": "grave",
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
_BUTTON_MAP: Dict[str, int] = {
|
|
687
|
+
"left": 1,
|
|
688
|
+
"middle": 2,
|
|
689
|
+
"right": 3,
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
def __init__(self) -> None:
|
|
693
|
+
self._x11 = ctypes.cdll.LoadLibrary("libX11.so.6")
|
|
694
|
+
self._xtst = ctypes.cdll.LoadLibrary("libXtst.so.6")
|
|
695
|
+
|
|
696
|
+
self._x11.XOpenDisplay.restype = ctypes.c_void_p
|
|
697
|
+
self._display = self._x11.XOpenDisplay(None)
|
|
698
|
+
if not self._display:
|
|
699
|
+
raise RuntimeError("Unable to open X11 display. Ensure DISPLAY is set.")
|
|
700
|
+
|
|
701
|
+
self._x11.XKeysymToKeycode.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
|
|
702
|
+
self._x11.XKeysymToKeycode.restype = ctypes.c_uint
|
|
703
|
+
self._x11.XStringToKeysym.argtypes = [ctypes.c_char_p]
|
|
704
|
+
self._x11.XStringToKeysym.restype = ctypes.c_ulong
|
|
705
|
+
self._xtst.XTestFakeRelativeMotionEvent.argtypes = [
|
|
706
|
+
ctypes.c_void_p,
|
|
707
|
+
ctypes.c_int,
|
|
708
|
+
ctypes.c_int,
|
|
709
|
+
ctypes.c_ulong,
|
|
710
|
+
]
|
|
711
|
+
self._xtst.XTestFakeButtonEvent.argtypes = [
|
|
712
|
+
ctypes.c_void_p,
|
|
713
|
+
ctypes.c_uint,
|
|
714
|
+
ctypes.c_int,
|
|
715
|
+
ctypes.c_ulong,
|
|
716
|
+
]
|
|
717
|
+
self._xtst.XTestFakeKeyEvent.argtypes = [
|
|
718
|
+
ctypes.c_void_p,
|
|
719
|
+
ctypes.c_uint,
|
|
720
|
+
ctypes.c_int,
|
|
721
|
+
ctypes.c_ulong,
|
|
722
|
+
]
|
|
723
|
+
self._x11.XFlush.argtypes = [ctypes.c_void_p]
|
|
724
|
+
self._x11.XDefaultScreen.argtypes = [ctypes.c_void_p]
|
|
725
|
+
self._x11.XDefaultScreen.restype = ctypes.c_int
|
|
726
|
+
self._x11.XDisplayWidth.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
|
727
|
+
self._x11.XDisplayWidth.restype = ctypes.c_int
|
|
728
|
+
self._x11.XDisplayHeight.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
|
729
|
+
self._x11.XDisplayHeight.restype = ctypes.c_int
|
|
730
|
+
self._x11.XRootWindow.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
|
731
|
+
self._x11.XRootWindow.restype = ctypes.c_ulong
|
|
732
|
+
self._x11.XQueryPointer.argtypes = [
|
|
733
|
+
ctypes.c_void_p,
|
|
734
|
+
ctypes.c_ulong,
|
|
735
|
+
ctypes.POINTER(ctypes.c_ulong),
|
|
736
|
+
ctypes.POINTER(ctypes.c_ulong),
|
|
737
|
+
ctypes.POINTER(ctypes.c_int),
|
|
738
|
+
ctypes.POINTER(ctypes.c_int),
|
|
739
|
+
ctypes.POINTER(ctypes.c_int),
|
|
740
|
+
ctypes.POINTER(ctypes.c_int),
|
|
741
|
+
ctypes.POINTER(ctypes.c_uint),
|
|
742
|
+
]
|
|
743
|
+
self._x11.XQueryPointer.restype = ctypes.c_bool
|
|
744
|
+
|
|
745
|
+
self._shift_keycode = self._resolve_keycode("Shift_L")
|
|
746
|
+
|
|
747
|
+
def _resolve_keycode(self, keysym_name: str) -> Optional[int]:
|
|
748
|
+
keysym = self._x11.XStringToKeysym(keysym_name.encode("ascii"))
|
|
749
|
+
if keysym == 0:
|
|
750
|
+
return None
|
|
751
|
+
keycode = self._x11.XKeysymToKeycode(self._display, keysym)
|
|
752
|
+
return int(keycode) if keycode else None
|
|
753
|
+
|
|
754
|
+
def _flush(self) -> None:
|
|
755
|
+
self._x11.XFlush(self._display)
|
|
756
|
+
|
|
757
|
+
def move_cursor(self, dx: float, dy: float) -> None:
|
|
758
|
+
self._xtst.XTestFakeRelativeMotionEvent(
|
|
759
|
+
self._display, int(round(dx)), int(round(dy)), 0
|
|
760
|
+
)
|
|
761
|
+
self._flush()
|
|
762
|
+
|
|
763
|
+
def click(self, button: str = "left", double: bool = False) -> None:
|
|
764
|
+
mapping = {"left": 1, "middle": 2, "right": 3}
|
|
765
|
+
if button not in mapping:
|
|
766
|
+
raise ValueError(f"Unsupported button: {button}")
|
|
767
|
+
button_code = mapping[button]
|
|
768
|
+
repeats = 2 if double else 1
|
|
769
|
+
for _ in range(repeats):
|
|
770
|
+
self._xtst.XTestFakeButtonEvent(self._display, button_code, True, 0)
|
|
771
|
+
self._xtst.XTestFakeButtonEvent(self._display, button_code, False, 0)
|
|
772
|
+
self._flush()
|
|
773
|
+
|
|
774
|
+
def button_action(self, button: str, action: str) -> None:
|
|
775
|
+
button_code = self._BUTTON_MAP.get(button)
|
|
776
|
+
if button_code is None:
|
|
777
|
+
raise ValueError(f"Unsupported button: {button}")
|
|
778
|
+
if action not in {"down", "up"}:
|
|
779
|
+
raise ValueError(f"Unsupported button action: {action}")
|
|
780
|
+
press = 1 if action == "down" else 0
|
|
781
|
+
self._xtst.XTestFakeButtonEvent(self._display, button_code, press, 0)
|
|
782
|
+
self._flush()
|
|
783
|
+
|
|
784
|
+
def scroll(self, vertical: float = 0.0, horizontal: float = 0.0) -> None:
|
|
785
|
+
def _emit(button: int, count: int) -> None:
|
|
786
|
+
for _ in range(abs(count)):
|
|
787
|
+
self._xtst.XTestFakeButtonEvent(self._display, button, True, 0)
|
|
788
|
+
self._xtst.XTestFakeButtonEvent(self._display, button, False, 0)
|
|
789
|
+
|
|
790
|
+
if vertical:
|
|
791
|
+
count = int(round(vertical))
|
|
792
|
+
if count == 0:
|
|
793
|
+
count = 1 if vertical > 0 else -1
|
|
794
|
+
button = 4 if count > 0 else 5
|
|
795
|
+
_emit(button, count)
|
|
796
|
+
if horizontal:
|
|
797
|
+
count = int(round(horizontal))
|
|
798
|
+
if count == 0:
|
|
799
|
+
count = 1 if horizontal > 0 else -1
|
|
800
|
+
button = 7 if count > 0 else 6
|
|
801
|
+
_emit(button, count)
|
|
802
|
+
self._flush()
|
|
803
|
+
|
|
804
|
+
def _press_keycode(self, keycode: int, shift: bool = False) -> None:
|
|
805
|
+
if shift and self._shift_keycode is not None:
|
|
806
|
+
self._xtst.XTestFakeKeyEvent(self._display, self._shift_keycode, True, 0)
|
|
807
|
+
self._xtst.XTestFakeKeyEvent(self._display, keycode, True, 0)
|
|
808
|
+
self._xtst.XTestFakeKeyEvent(self._display, keycode, False, 0)
|
|
809
|
+
if shift and self._shift_keycode is not None:
|
|
810
|
+
self._xtst.XTestFakeKeyEvent(self._display, self._shift_keycode, False, 0)
|
|
811
|
+
self._flush()
|
|
812
|
+
|
|
813
|
+
def type_text(self, text: str) -> None:
|
|
814
|
+
for char in text:
|
|
815
|
+
if char == "\n":
|
|
816
|
+
self.key_action("enter", "press")
|
|
817
|
+
continue
|
|
818
|
+
if char == "\r":
|
|
819
|
+
continue
|
|
820
|
+
if char == "\t":
|
|
821
|
+
self.key_action("tab", "press")
|
|
822
|
+
continue
|
|
823
|
+
if char == " ":
|
|
824
|
+
keysym = self._x11.XStringToKeysym(b"space")
|
|
825
|
+
else:
|
|
826
|
+
keysym = self._x11.XStringToKeysym(char.encode("utf-8"))
|
|
827
|
+
if keysym == 0:
|
|
828
|
+
continue
|
|
829
|
+
keycode = self._x11.XKeysymToKeycode(self._display, keysym)
|
|
830
|
+
if keycode == 0:
|
|
831
|
+
continue
|
|
832
|
+
need_shift = char in self._SHIFT_REQUIRED
|
|
833
|
+
self._press_keycode(keycode, shift=need_shift)
|
|
834
|
+
|
|
835
|
+
def key_action(self, key: str, action: str) -> None:
|
|
836
|
+
keysym_name = self._KEY_MAP.get(key)
|
|
837
|
+
if not keysym_name and len(key) == 1:
|
|
838
|
+
keysym_name = key
|
|
839
|
+
if not keysym_name:
|
|
840
|
+
raise ValueError(f"Unsupported key: {key}")
|
|
841
|
+
keycode = self._resolve_keycode(keysym_name)
|
|
842
|
+
if keycode is None:
|
|
843
|
+
raise RuntimeError(f"Could not resolve keycode for {keysym_name}")
|
|
844
|
+
if action == "press":
|
|
845
|
+
self._press_keycode(keycode)
|
|
846
|
+
return
|
|
847
|
+
if action == "down":
|
|
848
|
+
self._xtst.XTestFakeKeyEvent(self._display, keycode, True, 0)
|
|
849
|
+
elif action == "up":
|
|
850
|
+
self._xtst.XTestFakeKeyEvent(self._display, keycode, False, 0)
|
|
851
|
+
else:
|
|
852
|
+
raise ValueError(f"Unsupported action: {action}")
|
|
853
|
+
self._flush()
|
|
854
|
+
|
|
855
|
+
def state(self) -> Dict[str, float]:
|
|
856
|
+
screen = self._x11.XDefaultScreen(self._display)
|
|
857
|
+
width = float(self._x11.XDisplayWidth(self._display, screen))
|
|
858
|
+
height = float(self._x11.XDisplayHeight(self._display, screen))
|
|
859
|
+
root = self._x11.XRootWindow(self._display, screen)
|
|
860
|
+
root_return = ctypes.c_ulong()
|
|
861
|
+
child_return = ctypes.c_ulong()
|
|
862
|
+
root_x = ctypes.c_int()
|
|
863
|
+
root_y = ctypes.c_int()
|
|
864
|
+
win_x = ctypes.c_int()
|
|
865
|
+
win_y = ctypes.c_int()
|
|
866
|
+
mask = ctypes.c_uint()
|
|
867
|
+
success = self._x11.XQueryPointer(
|
|
868
|
+
self._display,
|
|
869
|
+
root,
|
|
870
|
+
ctypes.byref(root_return),
|
|
871
|
+
ctypes.byref(child_return),
|
|
872
|
+
ctypes.byref(root_x),
|
|
873
|
+
ctypes.byref(root_y),
|
|
874
|
+
ctypes.byref(win_x),
|
|
875
|
+
ctypes.byref(win_y),
|
|
876
|
+
ctypes.byref(mask),
|
|
877
|
+
)
|
|
878
|
+
if not success:
|
|
879
|
+
return {"x": 0.0, "y": 0.0, "width": width, "height": height}
|
|
880
|
+
return {
|
|
881
|
+
"x": float(root_x.value),
|
|
882
|
+
"y": float(root_y.value),
|
|
883
|
+
"width": width,
|
|
884
|
+
"height": height,
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _create_backend() -> _BaseBackend:
|
|
889
|
+
system = platform.system()
|
|
890
|
+
if system == "Windows":
|
|
891
|
+
return _WindowsBackend()
|
|
892
|
+
if system == "Darwin":
|
|
893
|
+
return _DarwinBackend()
|
|
894
|
+
return _X11Backend()
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
_BACKEND: Optional[_BaseBackend] = None
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _backend() -> _BaseBackend:
|
|
901
|
+
global _BACKEND
|
|
902
|
+
if _BACKEND is None:
|
|
903
|
+
_BACKEND = _create_backend()
|
|
904
|
+
return _BACKEND
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def move_cursor(dx: float, dy: float) -> Dict[str, float]:
|
|
908
|
+
backend = _backend()
|
|
909
|
+
if dx != 0 or dy != 0:
|
|
910
|
+
backend.move_cursor(dx, dy)
|
|
911
|
+
return backend.state()
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def click(button: str = "left", double: bool = False) -> None:
|
|
915
|
+
_backend().click(button=button, double=double)
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def button_action(button: str, action: str) -> None:
|
|
919
|
+
_backend().button_action(button=button, action=action)
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def scroll(vertical: float = 0.0, horizontal: float = 0.0) -> None:
|
|
923
|
+
_backend().scroll(vertical=vertical, horizontal=horizontal)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def type_text(text: str) -> None:
|
|
927
|
+
_backend().type_text(text)
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def key_action(key: str, action: str) -> None:
|
|
931
|
+
_backend().key_action(key, action)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def cursor_state() -> Dict[str, float]:
|
|
935
|
+
return _backend().state()
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def lock_screen() -> None:
|
|
939
|
+
system = platform.system()
|
|
940
|
+
if system == "Windows":
|
|
941
|
+
subprocess.run(
|
|
942
|
+
["rundll32.exe", "user32.dll,LockWorkStation"],
|
|
943
|
+
check=False,
|
|
944
|
+
)
|
|
945
|
+
return
|
|
946
|
+
|
|
947
|
+
if system == "Darwin":
|
|
948
|
+
mac_commands = [
|
|
949
|
+
[
|
|
950
|
+
"/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession",
|
|
951
|
+
"-suspend",
|
|
952
|
+
],
|
|
953
|
+
[
|
|
954
|
+
"osascript",
|
|
955
|
+
"-e",
|
|
956
|
+
'tell application "System Events" to keystroke "q" using {control down, command down}',
|
|
957
|
+
],
|
|
958
|
+
[
|
|
959
|
+
"pmset",
|
|
960
|
+
"displaysleepnow",
|
|
961
|
+
],
|
|
962
|
+
]
|
|
963
|
+
_run_first_success(
|
|
964
|
+
mac_commands,
|
|
965
|
+
error_message="Lock command unavailable on this system.",
|
|
966
|
+
)
|
|
967
|
+
return
|
|
968
|
+
|
|
969
|
+
_run_first_success(
|
|
970
|
+
[
|
|
971
|
+
["loginctl", "lock-session"],
|
|
972
|
+
["gnome-screensaver-command", "-l"],
|
|
973
|
+
["xdg-screensaver", "lock"],
|
|
974
|
+
],
|
|
975
|
+
error_message="Lock command unavailable on this system.",
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def shutdown_system() -> None:
|
|
980
|
+
system = platform.system()
|
|
981
|
+
if system == "Windows":
|
|
982
|
+
subprocess.run(["shutdown", "/s", "/t", "0"], check=False)
|
|
983
|
+
return
|
|
984
|
+
|
|
985
|
+
if system == "Darwin":
|
|
986
|
+
subprocess.run(
|
|
987
|
+
["osascript", "-e", 'tell application "System Events" to shut down'],
|
|
988
|
+
check=False,
|
|
989
|
+
)
|
|
990
|
+
return
|
|
991
|
+
|
|
992
|
+
_run_first_success(
|
|
993
|
+
[
|
|
994
|
+
["systemctl", "poweroff"],
|
|
995
|
+
["shutdown", "-h", "now"],
|
|
996
|
+
],
|
|
997
|
+
error_message="Shutdown command unavailable on this system.",
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _run_first_success(commands: Sequence[Sequence[str]], error_message: str) -> None:
|
|
1002
|
+
for command in commands:
|
|
1003
|
+
try:
|
|
1004
|
+
result = subprocess.run(command, check=False)
|
|
1005
|
+
except FileNotFoundError:
|
|
1006
|
+
continue
|
|
1007
|
+
if result.returncode == 0:
|
|
1008
|
+
return
|
|
1009
|
+
raise RuntimeError(error_message)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def unlock_screen() -> None:
|
|
1013
|
+
system = platform.system()
|
|
1014
|
+
if system == "Windows":
|
|
1015
|
+
try:
|
|
1016
|
+
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
|
1017
|
+
VK_SHIFT = 0x10
|
|
1018
|
+
KEYEVENTF_KEYUP = 0x0002
|
|
1019
|
+
user32.keybd_event(VK_SHIFT, 0, 0, 0)
|
|
1020
|
+
user32.keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0)
|
|
1021
|
+
except Exception:
|
|
1022
|
+
pass
|
|
1023
|
+
return
|
|
1024
|
+
|
|
1025
|
+
if system == "Darwin":
|
|
1026
|
+
mac_commands = [
|
|
1027
|
+
["osascript", "-e", 'tell application "System Events" to key code 49'],
|
|
1028
|
+
["caffeinate", "-u", "-t", "2"],
|
|
1029
|
+
]
|
|
1030
|
+
_run_first_success(
|
|
1031
|
+
mac_commands,
|
|
1032
|
+
error_message="Unlock command unavailable on this system.",
|
|
1033
|
+
)
|
|
1034
|
+
return
|
|
1035
|
+
|
|
1036
|
+
_run_first_success(
|
|
1037
|
+
[
|
|
1038
|
+
["loginctl", "unlock-session"],
|
|
1039
|
+
["gnome-screensaver-command", "-d"],
|
|
1040
|
+
["xdg-screensaver", "reset"],
|
|
1041
|
+
],
|
|
1042
|
+
error_message="Unlock command unavailable on this system.",
|
|
1043
|
+
)
|