AutoMonkey 0.4.1__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,301 @@
1
+ from os import remove
2
+ from os.path import isfile
3
+
4
+ from time import sleep
5
+
6
+ from pyautogui import center
7
+ from pyautogui import locateOnScreen
8
+
9
+ from pyscreeze import Box
10
+ from pytesseract import image_to_string
11
+
12
+ from clipboard import copy as copy1
13
+ from pyperclip import copy as copy2
14
+
15
+ try:
16
+ from PIL import Image
17
+ except ImportError:
18
+ import Image
19
+
20
+ import cv2
21
+ from cv2 import imread
22
+ from numpy import where
23
+
24
+ from pyautogui import screenshot
25
+
26
+ from .constants import IMG_EXT
27
+
28
+ # TODO: Add mousedown, mouseup, drag, and drop functions
29
+
30
+ def copy(text: str):
31
+ """Copy text to clipboard using two functions
32
+
33
+ Args:
34
+ text (str): text to be copied to clipboard
35
+ """
36
+ copy1(text)
37
+ copy2(text)
38
+
39
+ def _add_ext(filename: str) -> str:
40
+ """Adds extension to an image filename if missing
41
+
42
+ Args:
43
+ filename (str): image filename (with or without extension)
44
+
45
+ Returns:
46
+ str: image filename with extension if was missing
47
+ """
48
+
49
+ for ext in IMG_EXT:
50
+ if isfile(f"{filename}{ext}"):
51
+ return f"{filename}{ext}"
52
+ return filename
53
+
54
+
55
+ def is_on_screen(what: str) -> bool:
56
+ """Checks whether an image is found on screen
57
+ Args:
58
+ what (str): image to locate on screen
59
+ Returns:
60
+ bool: Returns True if image is found and False if not.
61
+ """
62
+
63
+ found = False
64
+ what = _add_ext(what)
65
+
66
+ if locateOnScreen(what, confidence=0.9) is not None:
67
+ found = True
68
+
69
+ return found
70
+
71
+
72
+ def get_center(image: str) -> tuple:
73
+ """Find the center of an image on screen
74
+ Args:
75
+ image ([str]): image to be located
76
+ Returns:
77
+ [point]: Returns the center of the located image as a point or None
78
+ """
79
+
80
+ image = _add_ext(image)
81
+
82
+ try:
83
+ if not isinstance(image, Box):
84
+ return center(locateOnScreen(image, confidence=0.9))
85
+ return center(image)
86
+ except TypeError:
87
+ return None
88
+ except NameError:
89
+ return None
90
+
91
+
92
+ def vertical_point(point, offset) -> tuple:
93
+ """Returns a PyAutoGUI point that is offset vertically
94
+ Args:
95
+ point (PyAutoGUI point): [A Tuple with an X and a Y]
96
+ offset (int): The offset. Can be positive or negative.
97
+ Positive = Above. Negative = Below
98
+ Returns:
99
+ [PyAutoGUI point]: The PyAutoGUI point offset vertically.
100
+ """
101
+ return point[0], point[1] - offset
102
+
103
+
104
+ def horizontal_point(point, offset) -> tuple:
105
+ """Returns a PyAutoGUI point that is offset horizontally
106
+ Args:
107
+ point (PyAutoGUI point): [A Tuple with an X and a Y]
108
+ offset (int): The offset. Can be positive or negative.
109
+ Positive = Right. Negative = Left
110
+ Returns:
111
+ [PyAutoGUI point]: The PyAutoGUI point offset horizontally.
112
+ """
113
+ return point[0] + offset, point[1]
114
+
115
+
116
+ def diagonal_point(point, x_point, y_point) -> tuple:
117
+ """Returns a PyAutoGUI point that is offset diagonally
118
+ Args:
119
+ point (PyAutoGUI point): [A Tuple with an X and a Y]
120
+ x_point (int): The horizontal offset. Can be positive or negative.
121
+ Positive = Right. Negative = Left
122
+ y_point (int): The vertical offset. Can be positive or negative.
123
+ Positive = Above. Negative = Below
124
+ Returns:
125
+ [PyAutoGUI point]: The PyAutoGUI point offset diagonally.
126
+ """
127
+ return point[0] + x_point, point[1] - y_point
128
+
129
+ def get_img_height(image_file: str) -> int:
130
+ """Function that returns the height of an image.
131
+ Args:
132
+ image_file (path): path to an image file, including filename.
133
+ Returns:
134
+ int: Height of the image
135
+ """
136
+
137
+ image_file = _add_ext(image_file)
138
+
139
+ img = Image.open(image_file)
140
+ _, height = img.size
141
+
142
+ return height
143
+
144
+
145
+ def get_img_width(image_file: str) -> int:
146
+ """Function that returns the width of an image.
147
+ Args:
148
+ image_file (path): path to an image file, including filename.
149
+ Returns:
150
+ int: Width of the image
151
+ """
152
+
153
+ image_file = _add_ext(image_file)
154
+
155
+ img = Image.open(image_file)
156
+ width, _ = img.size
157
+
158
+ return width
159
+
160
+ def __transform_region_1(*args) -> tuple:
161
+ assert isinstance(args[0], tuple), "The argument must be a tuple of 4 integers: Left, Top, Width, Height"
162
+ if len(args[0]) == 4:
163
+ region = (args[0][0], args[0][1], args[0][2] - args[0][0], args[0][3] - args[0][1])
164
+ elif len(args[0]) == 2:
165
+ region = __transform_region_4(args)
166
+ else:
167
+ region = args[0]
168
+ return region
169
+
170
+ # TODO: To have a function that can be called with different number and different types of arguments look into function overloading
171
+ # https://stackoverflow.com/questions/6434482/python-function-overloading
172
+ # This could be needed for:
173
+ # 1. get_text_from_region
174
+ # 2. copy_from_to
175
+
176
+ def __transform_region_2(*args) -> tuple:
177
+ assert isinstance(args[0], tuple), "The first argument must be a tuple of 2 integers: X, Y coordinates of the top left corner"
178
+ assert isinstance(args[1], tuple), "The second argument must be a tuple of 2 integers: X, Y coordinates of the bottom right corner"
179
+ return (args[0][0], args[0][1], args[1][0] - args[0][0], args[1][1] - args[0][1])
180
+
181
+ def __transform_region_3(*args) -> tuple:
182
+ assert isinstance(args[0], int), "The first argument must be an integer: X coordinate of the top left corner"
183
+ assert isinstance(args[1], int), "The second argument must be an integer: Y coordinate of the top left corner"
184
+ assert isinstance(args[2], int), "The third argument must be an integer: Width of the region"
185
+ assert isinstance(args[3], int), "The fourth argument must be an integer: Height of the region"
186
+ if args[2] > args[0] and args[3] > args[1]:
187
+ region = (args[0], args[1], args[2] - args[0], args[3] - args[1])
188
+ else:
189
+ region = (args[0], args[1], args[2], args[3])
190
+ return region
191
+
192
+ def __transform_region_4(*args) -> tuple:
193
+ if args[0][1][1] > args[0][0][0] and args[0][1][1] > args[0][0][1]:
194
+ region = (args[0][0][0], args[0][0][1], args[0][1][0] - args[0][0][0], args[0][1][1] - args[0][0][1])
195
+ else:
196
+ region = (args[0][0][0], args[0][0][1], args[0][1][0], args[0][1][1])
197
+ return region
198
+
199
+ def get_text_from_region(*args) -> str:
200
+ """Makes a screenshot of a screen region and performs OCR on it
201
+ Args:
202
+ Top left corner X, top left corner Y, bottom right corner X, bottom right corner Y
203
+ or
204
+ Top point (Tuple): (x, y) and Bottom point (Tuple): (x, y)
205
+ or
206
+ region (Tuple or PyAutoGUI region): (Left, Top, Width, Height)
207
+ or
208
+ Left (int), Top (int), Width (int), Height (int)
209
+ Returns:
210
+ str: The text from the region
211
+ """
212
+ if len(args) == 1:
213
+ region = __transform_region_1(args)
214
+ elif len(args) == 2:
215
+ region = __transform_region_2(args)
216
+ elif len(args) == 4:
217
+ region = __transform_region_3(args)
218
+
219
+ snap = screenshot(region=region)
220
+ snap.save("temp.jpg")
221
+ sleep(1)
222
+ img = imread('temp.jpg')
223
+
224
+ custom_config = r'--oem 3 --psm 6'
225
+ text = image_to_string(img, config=custom_config)
226
+ remove('temp.jpg')
227
+
228
+ copy(text) # in order to make the text available in the clipboard
229
+ return text
230
+
231
+
232
+ def count_img(needle: str, haystack: str = None) -> int:
233
+ """Counts how many times an image appears in a bigger image
234
+
235
+ Args:
236
+ needle (str): image filename to be counted
237
+ haystack (str): filename of image in which to search
238
+
239
+ Returns:
240
+ int: Count of occurrences of needle in the haystack
241
+ """
242
+
243
+ needle = _add_ext(needle)
244
+ if haystack:
245
+ haystack = _add_ext(haystack)
246
+ hay = imread(haystack)
247
+ else:
248
+ haystack = screenshot("temp.jpg")
249
+ hay = imread("temp.jpg")
250
+ remove("temp.jpg")
251
+
252
+ need = imread(needle)
253
+
254
+ res = cv2.cv2.matchTemplate(hay, need, cv2.cv2.TM_CCOEFF_NORMED)
255
+
256
+ threshold = .9 # 9 is more precise. 8 gives some false positives
257
+ loc = where(res >= threshold)
258
+
259
+ copy(len(loc[0])) # in order to make the text available in the clipboard
260
+ return len(loc[0])
261
+
262
+
263
+ def _offset_clicks(point: tuple, img: str, offset_value: str, click_type: str):
264
+ """Offset Clicks
265
+
266
+ Args:
267
+ point (tuple): PyAutoGUI Point as a (x, y) tuple
268
+ img (str): image path that was the source of the point
269
+ offset_value (str): Offset type:
270
+ - above
271
+ - bellow
272
+ - right
273
+ - left
274
+ - upper-left
275
+ - upper-right
276
+ - lower-left
277
+ - lower-right
278
+ click_type (str): click, rightClick, doubleClick, etc
279
+ """
280
+ __offset_clicks_1(point, img, offset_value, click_type)
281
+ __offset_clicks_2(point, img, offset_value, click_type)
282
+
283
+ def __offset_clicks_1(point: tuple, img: str, offset_value: str, click_type: str):
284
+ if offset_value == "above":
285
+ globals()[click_type](vertical_point(point, get_img_height(img)))
286
+ if offset_value == "bellow":
287
+ globals()[click_type](vertical_point(point, 0 - get_img_height(img)))
288
+ if offset_value == "right":
289
+ globals()[click_type](horizontal_point(point, get_img_width(img)))
290
+ if offset_value == "left":
291
+ globals()[click_type](horizontal_point(point, 0 - get_img_width(img)))
292
+
293
+ def __offset_clicks_2(point: tuple, img: str, offset_value: str, click_type: str):
294
+ if offset_value == "upper-left":
295
+ globals()[click_type](diagonal_point(point, 0 - get_img_width(img), get_img_height(img)))
296
+ if offset_value == "upper-right":
297
+ globals()[click_type](diagonal_point(point, get_img_width(img), get_img_height(img)))
298
+ if offset_value == "lower-left":
299
+ globals()[click_type](diagonal_point(point, 0 - get_img_width(img), 0 - get_img_height(img)))
300
+ if offset_value == "lower-right":
301
+ globals()[click_type](diagonal_point(point, get_img_width(img), 0 - get_img_height(img)))
@@ -0,0 +1,279 @@
1
+ """Keyboard-layout-aware typing, key presses and shortcuts.
2
+
3
+ pyautogui maps characters to keys with the layout of the *Python* thread (fixed at
4
+ import time) and sends bare virtual-key codes, which key remappers then rewrite. On
5
+ Windows that mangles text whenever the target window uses another layout (German
6
+ QWERTZ: ';' arrives as 'ö') or a remapper swaps keys (PowerToys y<->z: 'KEYBOARD'
7
+ arrives as 'KEZBOARD').
8
+
9
+ On Windows:
10
+ * text is injected as Unicode characters (KEYEVENTF_UNICODE): independent of the
11
+ layout and of key remappers, and it covers non-ASCII text (ă, ß, €) as well;
12
+ * keys and shortcuts are resolved with the layout of the *foreground* window, and
13
+ PowerToys Keyboard Manager single-key remaps are undone, so the app receives the
14
+ key that was asked for (ctrl+z stays undo when y and z are swapped);
15
+ * registry "Scancode Map" overrides rewrite hardware scancodes inside the keyboard
16
+ driver; injected input never passes through them, so they need no correction.
17
+
18
+ Set AUTOMONKEY_TYPING=keys (or pass mode="keys") to type text as real key presses
19
+ instead, for targets that ignore Unicode injection (some games, remote desktops, VMs).
20
+ Elsewhere this falls back to pyautogui, pasting text it cannot type.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import sys
27
+ import time
28
+
29
+ import pyautogui
30
+
31
+ _TEXT_KEYS = {"\n": "enter", "\t": "tab"}
32
+
33
+
34
+ def _mode(mode: str | None) -> str:
35
+ return (mode or os.environ.get("AUTOMONKEY_TYPING") or "unicode").lower()
36
+
37
+
38
+ def _flatten(keys) -> list[str]:
39
+ if len(keys) == 1 and isinstance(keys[0], (list, tuple)):
40
+ keys = keys[0]
41
+ return [str(k) for k in keys]
42
+
43
+
44
+ def _name(key: str) -> str:
45
+ return key if len(key) == 1 else key.lower()
46
+
47
+
48
+ if sys.platform == "win32":
49
+ import ctypes
50
+ from ctypes import wintypes
51
+
52
+ _u32 = ctypes.WinDLL("user32", use_last_error=True)
53
+ _k32 = ctypes.WinDLL("kernel32", use_last_error=True)
54
+
55
+ INPUT_KEYBOARD = 1
56
+ KEYEVENTF_EXTENDEDKEY = 0x1
57
+ KEYEVENTF_KEYUP = 0x2
58
+ KEYEVENTF_UNICODE = 0x4
59
+ MAPVK_VK_TO_VSC_EX = 4
60
+ VK_SHIFT, VK_CONTROL, VK_MENU = 0x10, 0x11, 0x12
61
+
62
+ class _MOUSEINPUT(ctypes.Structure):
63
+ _fields_ = [("dx", wintypes.LONG), ("dy", wintypes.LONG), ("mouseData", wintypes.DWORD),
64
+ ("dwFlags", wintypes.DWORD), ("time", wintypes.DWORD), ("dwExtraInfo", ctypes.c_size_t)]
65
+
66
+ class _KEYBDINPUT(ctypes.Structure):
67
+ _fields_ = [("wVk", wintypes.WORD), ("wScan", wintypes.WORD), ("dwFlags", wintypes.DWORD),
68
+ ("time", wintypes.DWORD), ("dwExtraInfo", ctypes.c_size_t)]
69
+
70
+ class _INPUTUNION(ctypes.Union): # sized by its largest member, as SendInput expects
71
+ _fields_ = [("mi", _MOUSEINPUT), ("ki", _KEYBDINPUT)]
72
+
73
+ class _INPUT(ctypes.Structure):
74
+ _fields_ = [("type", wintypes.DWORD), ("u", _INPUTUNION)]
75
+
76
+ class _PROCESSENTRY32W(ctypes.Structure):
77
+ _fields_ = [("dwSize", wintypes.DWORD), ("cntUsage", wintypes.DWORD), ("th32ProcessID", wintypes.DWORD),
78
+ ("th32DefaultHeapID", ctypes.c_size_t), ("th32ModuleID", wintypes.DWORD),
79
+ ("cntThreads", wintypes.DWORD), ("th32ParentProcessID", wintypes.DWORD),
80
+ ("pcPriClassBase", wintypes.LONG), ("dwFlags", wintypes.DWORD),
81
+ ("szExeFile", wintypes.WCHAR * 260)]
82
+
83
+ _u32.GetForegroundWindow.restype = wintypes.HWND
84
+ _u32.GetWindowThreadProcessId.argtypes = (wintypes.HWND, ctypes.POINTER(wintypes.DWORD))
85
+ _u32.GetWindowThreadProcessId.restype = wintypes.DWORD
86
+ _u32.GetKeyboardLayout.argtypes = (wintypes.DWORD,)
87
+ _u32.GetKeyboardLayout.restype = wintypes.HKL
88
+ _u32.VkKeyScanExW.argtypes = (wintypes.WCHAR, wintypes.HKL)
89
+ _u32.VkKeyScanExW.restype = ctypes.c_short
90
+ _u32.MapVirtualKeyExW.argtypes = (wintypes.UINT, wintypes.UINT, wintypes.HKL)
91
+ _u32.MapVirtualKeyExW.restype = wintypes.UINT
92
+ _u32.ToUnicodeEx.argtypes = (wintypes.UINT, wintypes.UINT, ctypes.POINTER(ctypes.c_ubyte),
93
+ wintypes.LPWSTR, ctypes.c_int, wintypes.UINT, wintypes.HKL)
94
+ _u32.SendInput.argtypes = (wintypes.UINT, ctypes.POINTER(_INPUT), ctypes.c_int)
95
+ _u32.SendInput.restype = wintypes.UINT
96
+ _k32.CreateToolhelp32Snapshot.argtypes = (wintypes.DWORD, wintypes.DWORD)
97
+ _k32.CreateToolhelp32Snapshot.restype = wintypes.HANDLE
98
+ _k32.Process32FirstW.argtypes = (wintypes.HANDLE, ctypes.POINTER(_PROCESSENTRY32W))
99
+ _k32.Process32NextW.argtypes = (wintypes.HANDLE, ctypes.POINTER(_PROCESSENTRY32W))
100
+ _k32.CloseHandle.argtypes = (wintypes.HANDLE,)
101
+
102
+ # Named keys ('enter', 'ctrl', 'f5', ...) have fixed virtual-key codes.
103
+ _NAMED = {k: v for k, v in pyautogui.platformModule.keyboardMapping.items() if len(k) > 1 and v}
104
+
105
+ def foreground_layout() -> int:
106
+ """Keyboard layout (HKL) of the window that will receive the input."""
107
+ hwnd = _u32.GetForegroundWindow()
108
+ tid = _u32.GetWindowThreadProcessId(hwnd, None) if hwnd else 0
109
+ return _u32.GetKeyboardLayout(tid)
110
+
111
+ def _running(exe: str) -> bool:
112
+ snap = _k32.CreateToolhelp32Snapshot(0x2, 0) # TH32CS_SNAPPROCESS
113
+ if snap in (None, wintypes.HANDLE(-1).value):
114
+ return False
115
+ try:
116
+ entry = _PROCESSENTRY32W(dwSize=ctypes.sizeof(_PROCESSENTRY32W))
117
+ ok = _k32.Process32FirstW(snap, ctypes.byref(entry))
118
+ while ok:
119
+ if entry.szExeFile.lower() == exe:
120
+ return True
121
+ ok = _k32.Process32NextW(snap, ctypes.byref(entry))
122
+ return False
123
+ finally:
124
+ _k32.CloseHandle(snap)
125
+
126
+ def powertoys_remaps(root: str | None = None) -> dict[int, int]:
127
+ """Single-key remaps PowerToys Keyboard Manager applies, {original_vk: new_vk}.
128
+ Its hook rewrites injected keys too, so these have to be undone. Empty when the
129
+ engine isn't running."""
130
+ root = root or os.path.join(os.environ.get("LOCALAPPDATA", ""), "Microsoft", "PowerToys", "Keyboard Manager")
131
+ try:
132
+ with open(os.path.join(root, "settings.json"), encoding="utf-8-sig") as f:
133
+ config = json.load(f)["properties"]["activeConfiguration"]["value"]
134
+ with open(os.path.join(root, f"{config}.json"), encoding="utf-8-sig") as f:
135
+ entries = json.load(f)["remapKeys"]["inProcess"]
136
+ except (OSError, ValueError, KeyError, TypeError):
137
+ return {}
138
+ remaps = {}
139
+ for e in entries:
140
+ new = str(e.get("newRemapKeys", ""))
141
+ if new.isdigit(): # "17;67" would be a key -> shortcut remap; leave those alone
142
+ remaps[int(e["originalKeys"])] = int(new)
143
+ return remaps
144
+
145
+ _remap_cache = (0.0, {})
146
+
147
+ def _remaps() -> dict[int, int]:
148
+ global _remap_cache
149
+ stamp, cached = _remap_cache
150
+ if time.monotonic() - stamp > 5:
151
+ cached = powertoys_remaps() if _running("powertoys.keyboardmanagerengine.exe") else {}
152
+ _remap_cache = (time.monotonic(), cached)
153
+ return cached
154
+
155
+ def _source_vk(vk: int, remaps: dict[int, int]) -> int:
156
+ """The key to inject so that, after remapping, the app receives `vk`."""
157
+ for orig, new in remaps.items():
158
+ if new == vk:
159
+ return orig
160
+ return vk
161
+
162
+ def _key(vk: int, up: bool, hkl, remaps) -> _INPUT:
163
+ vk = _source_vk(vk, remaps)
164
+ scan = _u32.MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, hkl)
165
+ flags = (KEYEVENTF_KEYUP if up else 0) | (KEYEVENTF_EXTENDEDKEY if scan >> 8 == 0xE0 else 0)
166
+ return _INPUT(type=INPUT_KEYBOARD, u=_INPUTUNION(ki=_KEYBDINPUT(wVk=vk, wScan=scan & 0xFF, dwFlags=flags)))
167
+
168
+ def _chord(vks: list[int], hkl, remaps) -> list[_INPUT]:
169
+ return [_key(vk, False, hkl, remaps) for vk in vks] + [_key(vk, True, hkl, remaps) for vk in reversed(vks)]
170
+
171
+ def _unicode(ch: str) -> list[_INPUT]:
172
+ data = ch.encode("utf-16-le") # characters beyond the BMP are two surrogate units
173
+ out = []
174
+ for i in range(0, len(data), 2):
175
+ unit = int.from_bytes(data[i:i + 2], "little")
176
+ for flags in (KEYEVENTF_UNICODE, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP):
177
+ out.append(_INPUT(type=INPUT_KEYBOARD, u=_INPUTUNION(ki=_KEYBDINPUT(wScan=unit, dwFlags=flags))))
178
+ return out
179
+
180
+ def _send(inputs: list[_INPUT]):
181
+ arr = (_INPUT * len(inputs))(*inputs)
182
+ if _u32.SendInput(len(inputs), arr, ctypes.sizeof(_INPUT)) != len(inputs):
183
+ raise OSError(f"SendInput failed: {ctypes.WinError(ctypes.get_last_error())}")
184
+
185
+ def char_keys(ch: str, hkl) -> list[int] | None:
186
+ """[modifier VKs..., VK] that type `ch` on layout `hkl`, or None if the layout
187
+ has no plain key for it (missing, or only reachable through a dead key)."""
188
+ if ord(ch) > 0xFFFF: # no layout has a key for a surrogate pair
189
+ return None
190
+ res = _u32.VkKeyScanExW(ch, hkl)
191
+ if res == -1:
192
+ return None
193
+ vk, state = res & 0xFF, (res >> 8) & 0xFF
194
+ if state & ~0x7: # Hankaku / layout-specific shift states we can't press
195
+ return None
196
+ mods = [m for bit, m in ((1, VK_SHIFT), (2, VK_CONTROL), (4, VK_MENU)) if state & bit]
197
+ keystate = (ctypes.c_ubyte * 256)()
198
+ for m in mods:
199
+ keystate[m] = 0x80
200
+ buf = ctypes.create_unicode_buffer(8)
201
+ # flag 0x4: don't touch the thread's dead-key state (Windows 10 1607+)
202
+ if _u32.ToUnicodeEx(vk, _u32.MapVirtualKeyExW(vk, 0, hkl), keystate, buf, 8, 0x4, hkl) < 0:
203
+ return None
204
+ return mods + [vk]
205
+
206
+ def _shortcut_vks(keys: list[str], hkl) -> list[int]:
207
+ vks = []
208
+ for key in keys:
209
+ if key in _NAMED:
210
+ add = [_NAMED[key]]
211
+ elif len(key) == 1 and key.isascii() and key.isalnum():
212
+ add = [ord(key.upper())] # apps match letter/digit shortcuts by VK on every layout
213
+ else: # punctuation: whatever key (and shift) types it on this layout
214
+ add = char_keys(key, hkl) if len(key) == 1 else None
215
+ if not add:
216
+ raise ValueError(f"key {key!r} is unknown or not on the active keyboard layout")
217
+ vks += [vk for vk in add if vk not in vks]
218
+ return vks
219
+
220
+ def write(text: str, interval: float = 0.01, mode: str | None = None):
221
+ """Type `text` exactly, whatever the keyboard layout or key remaps."""
222
+ hkl = foreground_layout()
223
+ remaps = _remaps()
224
+ as_keys = _mode(mode) == "keys"
225
+ for ch in text.replace("\r\n", "\n").replace("\r", "\n"):
226
+ pyautogui.failSafeCheck()
227
+ vks = [_NAMED[_TEXT_KEYS[ch]]] if ch in _TEXT_KEYS else char_keys(ch, hkl) if as_keys else None
228
+ _send(_chord(vks, hkl, remaps) if vks else _unicode(ch))
229
+ time.sleep(interval)
230
+ time.sleep(pyautogui.PAUSE)
231
+
232
+ def press(keys, presses: int = 1, interval: float = 0.0):
233
+ """Press and release each key in turn: a named key ('enter', 'f5') or a character."""
234
+ hkl = foreground_layout()
235
+ remaps = _remaps()
236
+ for _ in range(presses):
237
+ for key in [keys] if isinstance(keys, str) else keys:
238
+ pyautogui.failSafeCheck()
239
+ key = _name(key)
240
+ if key in _NAMED:
241
+ _send(_chord([_NAMED[key]], hkl, remaps))
242
+ elif len(key) == 1:
243
+ vks = char_keys(key, hkl)
244
+ _send(_chord(vks, hkl, remaps) if vks else _unicode(key))
245
+ else:
246
+ raise ValueError(f"unknown key {key!r}")
247
+ time.sleep(interval)
248
+ time.sleep(pyautogui.PAUSE)
249
+
250
+ def hotkey(*keys, interval: float = 0.0):
251
+ """Hold the keys down in order, then release them in reverse: hotkey('ctrl', 'z')."""
252
+ pyautogui.failSafeCheck()
253
+ hkl = foreground_layout()
254
+ remaps = _remaps()
255
+ vks = _shortcut_vks([_name(k) for k in _flatten(keys)], hkl)
256
+ if interval:
257
+ for inp in _chord(vks, hkl, remaps):
258
+ _send([inp])
259
+ time.sleep(interval)
260
+ else:
261
+ _send(_chord(vks, hkl, remaps))
262
+ time.sleep(pyautogui.PAUSE)
263
+
264
+ else:
265
+ def write(text: str, interval: float = 0.01, mode: str | None = None):
266
+ """Type `text`; pyautogui can't type non-ASCII (e.g. ă, ș), so that is pasted."""
267
+ if text.isascii():
268
+ pyautogui.write(text, interval=interval)
269
+ else:
270
+ import pyperclip
271
+ pyperclip.copy(text)
272
+ pyautogui.hotkey("ctrl", "v")
273
+
274
+ def press(keys, presses: int = 1, interval: float = 0.0):
275
+ pyautogui.press([_name(k) for k in ([keys] if isinstance(keys, str) else keys)],
276
+ presses=presses, interval=interval)
277
+
278
+ def hotkey(*keys, interval: float = 0.0):
279
+ pyautogui.hotkey(*[_name(k) for k in _flatten(keys)], interval=interval)
@@ -0,0 +1,37 @@
1
+ from pyautogui import move # It is used
2
+
3
+ def movedown(distance: int = 10, duration: float = 0.0):
4
+ """Moves the mouse down
5
+
6
+ Args:
7
+ distance (int, optional): Distance to move. Defaults to 10.
8
+ duration (float, optional): Duration of the movement in seconds. Defaults to 0.0.
9
+ """
10
+ move(0, int(distance), float(duration))
11
+
12
+ def moveleft(distance: int = 10, duration: float = 0.0):
13
+ """Moves the mouse left
14
+
15
+ Args:
16
+ distance (int, optional): Distance to move. Defaults to 10.
17
+ duration (float, optional): Duration of the movement in seconds. Defaults to 0.0.
18
+ """
19
+ move(-int(distance), 0, float(duration))
20
+
21
+ def moveright(distance: int = 10, duration: float = 0.0):
22
+ """Moves the mouse right
23
+
24
+ Args:
25
+ distance (int, optional): Distance to move. Defaults to 10.
26
+ duration (float, optional): Duration of the movement in seconds. Defaults to 0.0.
27
+ """
28
+ move(int(distance), 0, float(duration))
29
+
30
+ def moveup(distance: int = 10, duration: float = 0.0):
31
+ """Moves the mouse up
32
+
33
+ Args:
34
+ distance (int, optional): Distance to move. Defaults to 10.
35
+ duration (float, optional): Duration of the movement in seconds. Defaults to 0.0.
36
+ """
37
+ move(0, -int(distance), float(duration))