zhmiscellany 5.8.6__py3-none-any.whl → 6.0.9__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.
- zhmiscellany/_fileio_supportfuncs.py +4 -0
- zhmiscellany/_misc_supportfuncs.py +229 -107
- zhmiscellany/_processing_supportfuncs.py +26 -1
- zhmiscellany/discord.py +18 -4
- zhmiscellany/gui.py +84 -3
- zhmiscellany/macro.py +111 -35
- zhmiscellany/misc.py +26 -2
- zhmiscellany/netio.py +16 -8
- zhmiscellany/pipes.py +33 -4
- zhmiscellany/processing.py +16 -1
- {zhmiscellany-5.8.6.dist-info → zhmiscellany-6.0.9.dist-info}/METADATA +70 -6
- {zhmiscellany-5.8.6.dist-info → zhmiscellany-6.0.9.dist-info}/RECORD +14 -14
- {zhmiscellany-5.8.6.dist-info → zhmiscellany-6.0.9.dist-info}/WHEEL +0 -0
- {zhmiscellany-5.8.6.dist-info → zhmiscellany-6.0.9.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import threading, os, signal, time, sys, shutil, ctypes
|
|
1
|
+
import threading, os, signal, time, sys, shutil, ctypes, math
|
|
2
2
|
from ctypes import Structure, c_long, c_uint, c_int, POINTER, sizeof
|
|
3
3
|
import zhmiscellany.fileio
|
|
4
|
-
import
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
# Windows-specific imports
|
|
7
|
+
if sys.platform == "win32":
|
|
8
|
+
try:
|
|
9
|
+
import win32api
|
|
10
|
+
from ctypes import windll
|
|
11
|
+
WIN32_AVAILABLE = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
WIN32_AVAILABLE = False
|
|
14
|
+
print("Warning: Windows modules not available - Windows functionality disabled")
|
|
15
|
+
else:
|
|
16
|
+
WIN32_AVAILABLE = False
|
|
5
17
|
|
|
6
18
|
|
|
7
19
|
_misc_action = 0
|
|
@@ -48,7 +60,8 @@ def patch_rhg(): # patches random_header_generator library's missing files. thi
|
|
|
48
60
|
else:
|
|
49
61
|
# we are running in normal Python environment
|
|
50
62
|
pass
|
|
51
|
-
|
|
63
|
+
if WIN32_AVAILABLE:
|
|
64
|
+
patch_rhg()
|
|
52
65
|
|
|
53
66
|
|
|
54
67
|
def patch_cpp():
|
|
@@ -75,111 +88,219 @@ def patch_cpp():
|
|
|
75
88
|
gen()
|
|
76
89
|
shutil.copy2(os.path.join(base_path, 'resources', fn), tp)
|
|
77
90
|
os.chdir(cwd)
|
|
78
|
-
patch_cpp()
|
|
79
91
|
|
|
92
|
+
if WIN32_AVAILABLE:
|
|
93
|
+
patch_cpp()
|
|
94
|
+
|
|
95
|
+
import ctypes
|
|
96
|
+
import os
|
|
97
|
+
import math
|
|
98
|
+
import sys
|
|
99
|
+
from ctypes import c_int, c_uint, c_long, POINTER, Structure, sizeof, byref, c_ulong, c_void_p
|
|
100
|
+
|
|
101
|
+
# --- OS Detection ---
|
|
102
|
+
IS_LINUX = not WIN32_AVAILABLE
|
|
103
|
+
|
|
104
|
+
# --- Windows Imports & Structures ---
|
|
105
|
+
if WIN32_AVAILABLE:
|
|
106
|
+
windll = ctypes.windll
|
|
107
|
+
|
|
108
|
+
if WIN32_AVAILABLE:
|
|
109
|
+
class POINT(Structure):
|
|
110
|
+
_fields_ = [("x", c_long),
|
|
111
|
+
("y", c_long)]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MOUSEINPUT(Structure):
|
|
115
|
+
_fields_ = [("dx", c_long),
|
|
116
|
+
("dy", c_long),
|
|
117
|
+
("mouseData", c_uint),
|
|
118
|
+
("dwFlags", c_uint),
|
|
119
|
+
("time", c_uint),
|
|
120
|
+
("dwExtraInfo", POINTER(c_uint))]
|
|
80
121
|
|
|
81
|
-
class POINT(Structure):
|
|
82
|
-
_fields_ = [("x", c_long),
|
|
83
|
-
("y", c_long)]
|
|
84
122
|
|
|
123
|
+
class INPUT_UNION(ctypes.Union):
|
|
124
|
+
_fields_ = [("mi", MOUSEINPUT)]
|
|
85
125
|
|
|
86
|
-
class MOUSEINPUT(Structure):
|
|
87
|
-
_fields_ = [("dx", c_long),
|
|
88
|
-
("dy", c_long),
|
|
89
|
-
("mouseData", c_uint),
|
|
90
|
-
("dwFlags", c_uint),
|
|
91
|
-
("time", c_uint),
|
|
92
|
-
("dwExtraInfo", POINTER(c_uint))]
|
|
93
126
|
|
|
127
|
+
class INPUT(Structure):
|
|
128
|
+
_fields_ = [("type", c_int),
|
|
129
|
+
("union", INPUT_UNION)]
|
|
94
130
|
|
|
95
|
-
class INPUT_UNION(ctypes.Union):
|
|
96
|
-
_fields_ = [("mi", MOUSEINPUT)]
|
|
97
131
|
|
|
132
|
+
# Windows Constants
|
|
133
|
+
MOUSEEVENTF_ABSOLUTE = 0x8000
|
|
134
|
+
MOUSEEVENTF_MOVE = 0x0001
|
|
135
|
+
MOUSEEVENTF_LEFTDOWN = 0x0002
|
|
136
|
+
MOUSEEVENTF_LEFTUP = 0x0004
|
|
137
|
+
MOUSEEVENTF_RIGHTDOWN = 0x0008
|
|
138
|
+
MOUSEEVENTF_RIGHTUP = 0x0010
|
|
139
|
+
MOUSEEVENTF_MIDDLEDOWN = 0x0020
|
|
140
|
+
MOUSEEVENTF_MIDDLEUP = 0x0040
|
|
98
141
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
142
|
+
# --- Linux / X11 Setup ---
|
|
143
|
+
if IS_LINUX:
|
|
144
|
+
try:
|
|
145
|
+
# Load X11 and XTest (required for fake inputs)
|
|
146
|
+
x11 = ctypes.cdll.LoadLibrary("libX11.so.6")
|
|
147
|
+
xtst = ctypes.cdll.LoadLibrary("libXtst.so.6")
|
|
102
148
|
|
|
149
|
+
# Setup Display
|
|
150
|
+
display = x11.XOpenDisplay(None)
|
|
151
|
+
root_window = x11.XDefaultRootWindow(display)
|
|
103
152
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
MOUSEEVENTF_LEFTUP = 0x0004
|
|
109
|
-
MOUSEEVENTF_RIGHTDOWN = 0x0008
|
|
110
|
-
MOUSEEVENTF_RIGHTUP = 0x0010
|
|
111
|
-
MOUSEEVENTF_MIDDLEDOWN = 0x0020
|
|
112
|
-
MOUSEEVENTF_MIDDLEUP = 0x0040
|
|
153
|
+
except OSError:
|
|
154
|
+
print("Error: Could not load X11 or Xtst libraries. Ensure libx11-6 and libxtst6 are installed.")
|
|
155
|
+
x11 = None
|
|
156
|
+
xtst = None
|
|
113
157
|
|
|
114
|
-
|
|
158
|
+
|
|
159
|
+
# --- Screen Metrics ---
|
|
115
160
|
def get_actual_screen_resolution():
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
if WIN32_AVAILABLE:
|
|
162
|
+
if WIN32_AVAILABLE:
|
|
163
|
+
hdc = windll.user32.GetDC(0)
|
|
164
|
+
width = windll.gdi32.GetDeviceCaps(hdc, 118) # HORZRES
|
|
165
|
+
height = windll.gdi32.GetDeviceCaps(hdc, 117) # VERTRES
|
|
166
|
+
windll.user32.ReleaseDC(0, hdc)
|
|
167
|
+
return width, height
|
|
168
|
+
return 1920, 1080
|
|
169
|
+
elif IS_LINUX and x11:
|
|
170
|
+
screen_num = 0
|
|
171
|
+
width = x11.XDisplayWidth(display, screen_num)
|
|
172
|
+
height = x11.XDisplayHeight(display, screen_num)
|
|
173
|
+
return width, height
|
|
174
|
+
else:
|
|
175
|
+
return 1920, 1080
|
|
176
|
+
|
|
121
177
|
|
|
122
178
|
SCREEN_WIDTH, SCREEN_HEIGHT = get_actual_screen_resolution()
|
|
123
179
|
|
|
180
|
+
# --- Globals ---
|
|
124
181
|
calibrated = False
|
|
125
182
|
calipass = False
|
|
183
|
+
calibration_multiplier_x = 1.0
|
|
184
|
+
calibration_multiplier_y = 1.0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# --- Functions ---
|
|
126
188
|
|
|
127
189
|
def move_mouse(x: int, y: int, relative=False):
|
|
128
|
-
if
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if calibrated:
|
|
135
|
-
normalized_x = math.ceil(x * calibration_multiplier_x)
|
|
136
|
-
normalized_y = math.ceil(y * calibration_multiplier_y)
|
|
190
|
+
if WIN32_AVAILABLE:
|
|
191
|
+
if not relative:
|
|
192
|
+
# Convert coordinates to normalized coordinates (0-65535)
|
|
193
|
+
normalized_x = int(x * (65535 / SCREEN_WIDTH))
|
|
194
|
+
normalized_y = int(y * (65535 / SCREEN_HEIGHT))
|
|
195
|
+
dwflags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE
|
|
137
196
|
else:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
197
|
+
calibrate()
|
|
198
|
+
if calibrated:
|
|
199
|
+
normalized_x = math.ceil(x * calibration_multiplier_x)
|
|
200
|
+
normalized_y = math.ceil(y * calibration_multiplier_y)
|
|
201
|
+
else:
|
|
202
|
+
normalized_x = x
|
|
203
|
+
normalized_y = y
|
|
204
|
+
dwflags = MOUSEEVENTF_MOVE
|
|
205
|
+
|
|
206
|
+
input_struct = INPUT(
|
|
207
|
+
type=0,
|
|
208
|
+
union=INPUT_UNION(
|
|
209
|
+
mi=MOUSEINPUT(
|
|
210
|
+
dx=normalized_x,
|
|
211
|
+
dy=normalized_y,
|
|
212
|
+
mouseData=0,
|
|
213
|
+
dwFlags=dwflags,
|
|
214
|
+
time=0,
|
|
215
|
+
dwExtraInfo=None
|
|
216
|
+
)
|
|
156
217
|
)
|
|
157
218
|
)
|
|
158
|
-
|
|
219
|
+
windll.user32.SendInput(1, ctypes.byref(input_struct), sizeof(INPUT))
|
|
220
|
+
|
|
221
|
+
elif IS_LINUX and x11:
|
|
222
|
+
# XWarpPointer(display, src_w, dest_w, src_x, src_y, src_width, src_height, dest_x, dest_y)
|
|
223
|
+
if relative:
|
|
224
|
+
# Move relative to current position (None as dest_window usually means relative move,
|
|
225
|
+
# but standard practice is None src, None dest = move relative to current position)
|
|
226
|
+
x11.XWarpPointer(display, None, None, 0, 0, 0, 0, int(x), int(y))
|
|
227
|
+
else:
|
|
228
|
+
# Absolute move relative to Root Window
|
|
229
|
+
x11.XWarpPointer(display, None, root_window, 0, 0, 0, 0, int(x), int(y))
|
|
230
|
+
|
|
231
|
+
x11.XFlush(display)
|
|
159
232
|
|
|
160
|
-
ctypes.windll.user32.SendInput(1, ctypes.byref(input_struct), sizeof(INPUT))
|
|
161
233
|
|
|
162
234
|
def get_mouse_xy():
|
|
163
|
-
|
|
164
|
-
|
|
235
|
+
if WIN32_AVAILABLE:
|
|
236
|
+
if WIN32_AVAILABLE:
|
|
237
|
+
return win32api.GetCursorPos()
|
|
238
|
+
return 0, 0
|
|
239
|
+
elif IS_LINUX and x11:
|
|
240
|
+
root_id = c_ulong()
|
|
241
|
+
child_id = c_ulong()
|
|
242
|
+
root_x = c_int()
|
|
243
|
+
root_y = c_int()
|
|
244
|
+
win_x = c_int()
|
|
245
|
+
win_y = c_int()
|
|
246
|
+
mask = c_uint()
|
|
247
|
+
|
|
248
|
+
# XQueryPointer returns True if pointer is on the same screen
|
|
249
|
+
result = x11.XQueryPointer(display, root_window,
|
|
250
|
+
byref(root_id), byref(child_id),
|
|
251
|
+
byref(root_x), byref(root_y),
|
|
252
|
+
byref(win_x), byref(win_y),
|
|
253
|
+
byref(mask))
|
|
254
|
+
if result:
|
|
255
|
+
return root_x.value, root_y.value
|
|
256
|
+
return 0, 0
|
|
257
|
+
else:
|
|
258
|
+
return 0, 0
|
|
259
|
+
|
|
165
260
|
|
|
166
261
|
def calibrate():
|
|
167
262
|
global calibration_multiplier_x, calibration_multiplier_y, calibrated, calipass
|
|
263
|
+
|
|
264
|
+
# Linux (X11) relative movement is usually 1:1 pixel accurate via XWarpPointer
|
|
265
|
+
# so we skip the calibration routine.
|
|
266
|
+
if IS_LINUX:
|
|
267
|
+
calibrated = True
|
|
268
|
+
calipass = True
|
|
269
|
+
calibration_multiplier_x = 1.0
|
|
270
|
+
calibration_multiplier_y = 1.0
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
if not WIN32_AVAILABLE:
|
|
274
|
+
return
|
|
275
|
+
|
|
168
276
|
if calibrated:
|
|
169
277
|
return
|
|
170
278
|
if calipass:
|
|
171
279
|
return
|
|
280
|
+
|
|
172
281
|
calipass = True
|
|
173
282
|
# calibrate relative movement, required because windows is weird
|
|
174
283
|
original_mouse_point = get_mouse_xy()
|
|
175
284
|
calibration_distance = 128
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
285
|
+
moved_pos = (0, 0)
|
|
286
|
+
lim = 64
|
|
287
|
+
i = 0
|
|
288
|
+
while ((not moved_pos[0]) or (not moved_pos[1])) and i < lim:
|
|
289
|
+
i += 1
|
|
290
|
+
move_mouse(0, 0)
|
|
291
|
+
move_mouse(calibration_distance, calibration_distance, relative=True)
|
|
292
|
+
moved_pos = get_mouse_xy()
|
|
293
|
+
if not i < lim:
|
|
294
|
+
raise Exception('Relative mouse movement could not be calibrated.')
|
|
295
|
+
|
|
296
|
+
# Avoid division by zero
|
|
297
|
+
if moved_pos[0] != 0:
|
|
298
|
+
calibration_multiplier_x = calibration_distance / moved_pos[0]
|
|
299
|
+
if moved_pos[1] != 0:
|
|
300
|
+
calibration_multiplier_y = calibration_distance / moved_pos[1]
|
|
301
|
+
|
|
181
302
|
calibrated = True
|
|
182
|
-
move_mouse(original_mouse_point[0]
|
|
303
|
+
move_mouse(original_mouse_point[0], original_mouse_point[1])
|
|
183
304
|
|
|
184
305
|
|
|
185
306
|
def mouse_down(button: int):
|
|
@@ -187,27 +308,28 @@ def mouse_down(button: int):
|
|
|
187
308
|
if button not in [1, 2, 3]:
|
|
188
309
|
raise ValueError("Button must be 1 (left), 2 (right), or 3 (middle)")
|
|
189
310
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
dx=0,
|
|
201
|
-
dy=0,
|
|
202
|
-
mouseData=0,
|
|
203
|
-
dwFlags=flags[button],
|
|
204
|
-
time=0,
|
|
205
|
-
dwExtraInfo=None
|
|
311
|
+
if WIN32_AVAILABLE:
|
|
312
|
+
flags = {
|
|
313
|
+
1: MOUSEEVENTF_LEFTDOWN,
|
|
314
|
+
2: MOUSEEVENTF_RIGHTDOWN,
|
|
315
|
+
3: MOUSEEVENTF_MIDDLEDOWN
|
|
316
|
+
}
|
|
317
|
+
input_struct = INPUT(
|
|
318
|
+
type=0,
|
|
319
|
+
union=INPUT_UNION(
|
|
320
|
+
mi=MOUSEINPUT(dx=0, dy=0, mouseData=0, dwFlags=flags[button], time=0, dwExtraInfo=None)
|
|
206
321
|
)
|
|
207
322
|
)
|
|
208
|
-
|
|
323
|
+
windll.user32.SendInput(1, ctypes.byref(input_struct), sizeof(INPUT))
|
|
324
|
+
|
|
325
|
+
elif IS_LINUX and xtst:
|
|
326
|
+
# Linux X11 Button Mapping: 1=Left, 2=Middle, 3=Right
|
|
327
|
+
# Input Mapping: 1=Left, 2=Right, 3=Middle
|
|
328
|
+
linux_button_map = {1: 1, 2: 3, 3: 2}
|
|
209
329
|
|
|
210
|
-
|
|
330
|
+
# XTestFakeButtonEvent(display, button, is_press, delay)
|
|
331
|
+
xtst.XTestFakeButtonEvent(display, linux_button_map[button], True, 0)
|
|
332
|
+
x11.XFlush(display)
|
|
211
333
|
|
|
212
334
|
|
|
213
335
|
def mouse_up(button: int):
|
|
@@ -215,24 +337,24 @@ def mouse_up(button: int):
|
|
|
215
337
|
if button not in [1, 2, 3]:
|
|
216
338
|
raise ValueError("Button must be 1 (left), 2 (right), or 3 (middle)")
|
|
217
339
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
dx=0,
|
|
229
|
-
dy=0,
|
|
230
|
-
mouseData=0,
|
|
231
|
-
dwFlags=flags[button],
|
|
232
|
-
time=0,
|
|
233
|
-
dwExtraInfo=None
|
|
340
|
+
if WIN32_AVAILABLE:
|
|
341
|
+
flags = {
|
|
342
|
+
1: MOUSEEVENTF_LEFTUP,
|
|
343
|
+
2: MOUSEEVENTF_RIGHTUP,
|
|
344
|
+
3: MOUSEEVENTF_MIDDLEUP
|
|
345
|
+
}
|
|
346
|
+
input_struct = INPUT(
|
|
347
|
+
type=0,
|
|
348
|
+
union=INPUT_UNION(
|
|
349
|
+
mi=MOUSEINPUT(dx=0, dy=0, mouseData=0, dwFlags=flags[button], time=0, dwExtraInfo=None)
|
|
234
350
|
)
|
|
235
351
|
)
|
|
236
|
-
|
|
352
|
+
windll.user32.SendInput(1, ctypes.byref(input_struct), sizeof(INPUT))
|
|
353
|
+
|
|
354
|
+
elif IS_LINUX and xtst:
|
|
355
|
+
# Linux X11 Button Mapping: 1=Left, 2=Middle, 3=Right
|
|
356
|
+
linux_button_map = {1: 1, 2: 3, 3: 2}
|
|
237
357
|
|
|
238
|
-
|
|
358
|
+
# XTestFakeButtonEvent(display, button, is_press, delay)
|
|
359
|
+
xtst.XTestFakeButtonEvent(display, linux_button_map[button], False, 0)
|
|
360
|
+
x11.XFlush(display)
|
|
@@ -7,6 +7,16 @@ import sys
|
|
|
7
7
|
import io
|
|
8
8
|
from unittest.mock import patch
|
|
9
9
|
|
|
10
|
+
# Ray availability check
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
try:
|
|
13
|
+
import ray
|
|
14
|
+
RAY_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
RAY_AVAILABLE = False
|
|
17
|
+
else:
|
|
18
|
+
RAY_AVAILABLE = False
|
|
19
|
+
|
|
10
20
|
|
|
11
21
|
def clear_logs():
|
|
12
22
|
ray_dir = tempfile.gettempdir()
|
|
@@ -59,6 +69,10 @@ def safe_open_log(path, unbuffered=False, **kwargs):
|
|
|
59
69
|
|
|
60
70
|
|
|
61
71
|
def ray_init(auto=False):
|
|
72
|
+
if not RAY_AVAILABLE:
|
|
73
|
+
print("ray_init() only supports Windows! Functionality disabled")
|
|
74
|
+
return
|
|
75
|
+
|
|
62
76
|
if auto:
|
|
63
77
|
if 'in_ray_matrix' in os.environ:
|
|
64
78
|
return
|
|
@@ -184,6 +198,10 @@ class ThreadWithResult(threading.Thread):
|
|
|
184
198
|
|
|
185
199
|
|
|
186
200
|
def batch_multiprocess(targets_and_args, max_retries=0, expect_crashes=False, disable_warning=False, flatten=False):
|
|
201
|
+
if not RAY_AVAILABLE:
|
|
202
|
+
print("batch_multiprocess() only supports Windows! Returning empty list")
|
|
203
|
+
return []
|
|
204
|
+
|
|
187
205
|
if _ray_state == 'disabled':
|
|
188
206
|
if not disable_warning:
|
|
189
207
|
logging.warning("zhmiscellany didn't detect that you were going to be using multiprocessing functions, and ray was not initialized preemptively.\n\
|
|
@@ -231,6 +249,9 @@ from zhmiscellany._processing_supportfuncs import _ray_init_thread; _ray_init_th
|
|
|
231
249
|
return results
|
|
232
250
|
|
|
233
251
|
def multiprocess(target, args=(), max_retries=0, disable_warning=False):
|
|
252
|
+
if not RAY_AVAILABLE:
|
|
253
|
+
print("multiprocess() only supports Windows! Returning None")
|
|
254
|
+
return None
|
|
234
255
|
return batch_multiprocess([(target, args)], disable_warning=disable_warning, max_retries=max_retries)[0]
|
|
235
256
|
|
|
236
257
|
|
|
@@ -257,6 +278,10 @@ class RayActorWrapper:
|
|
|
257
278
|
|
|
258
279
|
|
|
259
280
|
def synchronous_class_multiprocess(cls, *args, disable_warning=False, **kwargs):
|
|
281
|
+
if not RAY_AVAILABLE:
|
|
282
|
+
print("synchronous_class_multiprocess() only supports Windows! Returning None")
|
|
283
|
+
return None
|
|
284
|
+
|
|
260
285
|
if _ray_state == 'disabled':
|
|
261
286
|
if not disable_warning:
|
|
262
287
|
logging.warning("zhmiscellany didn't detect that you were going to be using multiprocessing functions, and ray was not initialized preemptively.\n\
|
|
@@ -278,4 +303,4 @@ from zhmiscellany._processing_supportfuncs import _ray_init_thread; _ray_init_th
|
|
|
278
303
|
|
|
279
304
|
remote_cls = ray.remote(cls)
|
|
280
305
|
actor_instance = remote_cls.remote(*args, **kwargs)
|
|
281
|
-
return RayActorWrapper(actor_instance)
|
|
306
|
+
return RayActorWrapper(actor_instance)
|
zhmiscellany/discord.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
|
-
|
|
2
|
+
import sys
|
|
3
3
|
import requests
|
|
4
4
|
import copy
|
|
5
5
|
import zhmiscellany.fileio
|
|
@@ -8,11 +8,21 @@ import zhmiscellany.processing
|
|
|
8
8
|
from ._discord_supportfuncs import scrape_guild
|
|
9
9
|
|
|
10
10
|
import base64
|
|
11
|
-
import Crypto.Cipher.AES
|
|
12
11
|
import os
|
|
13
12
|
import json
|
|
14
13
|
import re
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
# Windows-specific imports
|
|
16
|
+
if sys.platform == "win32":
|
|
17
|
+
try:
|
|
18
|
+
import win32crypt
|
|
19
|
+
from Crypto.Cipher import AES
|
|
20
|
+
WIN32_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
WIN32_AVAILABLE = False
|
|
23
|
+
print("Warning: Windows modules not available - local Discord user detection disabled")
|
|
24
|
+
else:
|
|
25
|
+
WIN32_AVAILABLE = False
|
|
16
26
|
|
|
17
27
|
|
|
18
28
|
def add_reactions_to_message(user_token, emojis, channel_id, message_id):
|
|
@@ -108,6 +118,10 @@ def get_channel_messages(user_token, channel_id, limit=0, use_cache=True, show_p
|
|
|
108
118
|
|
|
109
119
|
|
|
110
120
|
def get_local_discord_user(show_output=False):
|
|
121
|
+
if not WIN32_AVAILABLE:
|
|
122
|
+
print("get_local_discord_user() only supports Windows! Returning None")
|
|
123
|
+
return None
|
|
124
|
+
|
|
111
125
|
global _cached_user_info
|
|
112
126
|
try:
|
|
113
127
|
a = _cached_user_info
|
|
@@ -121,7 +135,7 @@ def get_local_discord_user(show_output=False):
|
|
|
121
135
|
|
|
122
136
|
def decrypt(buff, master_key):
|
|
123
137
|
try:
|
|
124
|
-
return
|
|
138
|
+
return AES.new(win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1], AES.MODE_GCM, buff[3:15]).decrypt(buff[15:])[:-16].decode()
|
|
125
139
|
except:
|
|
126
140
|
return "Error"
|
|
127
141
|
|
zhmiscellany/gui.py
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import tkinter as tk
|
|
2
2
|
import threading
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Windows-specific imports
|
|
6
|
+
if sys.platform == "win32":
|
|
7
|
+
try:
|
|
8
|
+
import ctypes
|
|
9
|
+
from ctypes import wintypes
|
|
10
|
+
import win32gui
|
|
11
|
+
WIN32_AVAILABLE = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
WIN32_AVAILABLE = False
|
|
14
|
+
print("Warning: Windows modules not available - GUI functionality disabled")
|
|
15
|
+
else:
|
|
16
|
+
WIN32_AVAILABLE = False
|
|
5
17
|
|
|
6
18
|
|
|
7
19
|
class StateIndicator:
|
|
@@ -64,6 +76,10 @@ class StateIndicator:
|
|
|
64
76
|
|
|
65
77
|
def _make_click_through(self):
|
|
66
78
|
"""Make the window click-through using Windows API"""
|
|
79
|
+
if not WIN32_AVAILABLE:
|
|
80
|
+
print("Click-through only supported on Windows")
|
|
81
|
+
return
|
|
82
|
+
|
|
67
83
|
try:
|
|
68
84
|
# Get the window handle
|
|
69
85
|
hwnd = ctypes.windll.user32.GetParent(self._root.winfo_id())
|
|
@@ -125,6 +141,9 @@ class StateIndicator:
|
|
|
125
141
|
|
|
126
142
|
def _update_layered_attributes(self):
|
|
127
143
|
"""Update the layered window attributes for proper transparency with click-through"""
|
|
144
|
+
if not WIN32_AVAILABLE:
|
|
145
|
+
return
|
|
146
|
+
|
|
128
147
|
try:
|
|
129
148
|
hwnd = ctypes.windll.user32.GetParent(self._root.winfo_id())
|
|
130
149
|
if hwnd == 0:
|
|
@@ -176,4 +195,66 @@ class StateIndicator:
|
|
|
176
195
|
if isinstance(xy_tuple, tuple) and len(xy_tuple) == 2 and all(isinstance(n, (int, float)) and n > 0 for n in xy_tuple):
|
|
177
196
|
self._size = xy_tuple
|
|
178
197
|
if self._root and self._root.winfo_exists():
|
|
179
|
-
self._root.after(0, self._update_size_internal)
|
|
198
|
+
self._root.after(0, self._update_size_internal)
|
|
199
|
+
|
|
200
|
+
if WIN32_AVAILABLE:
|
|
201
|
+
user32 = ctypes.windll.user32
|
|
202
|
+
|
|
203
|
+
def get_focused_window():
|
|
204
|
+
"""Return the window that currently has the keyboard focus."""
|
|
205
|
+
return user32.GetForegroundWindow()
|
|
206
|
+
|
|
207
|
+
def get_window_rect(hwnd):
|
|
208
|
+
"""Return the bounding rectangle of a window."""
|
|
209
|
+
HWND = wintypes.HWND
|
|
210
|
+
RECT = wintypes.RECT
|
|
211
|
+
rect = RECT()
|
|
212
|
+
user32.GetWindowRect(hwnd, ctypes.byref(rect))
|
|
213
|
+
return rect
|
|
214
|
+
|
|
215
|
+
def set_window_pos(hwnd, x: int, y: int, w: int, h: int):
|
|
216
|
+
"""Move (and optionally resize) a window."""
|
|
217
|
+
# 0x0040 == SWP_NOACTIVATE | 0x0020 == SWP_SHOWWINDOW
|
|
218
|
+
user32.SetWindowPos(hwnd, 0, x, y, w, h, 0x0040 | 0x0020)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def find_window_by_title_fuzzy(title_query, threshold=70):
|
|
222
|
+
from fuzzywuzzy import process
|
|
223
|
+
from fuzzywuzzy import fuzz
|
|
224
|
+
def enum_windows_callback(hwnd, windows):
|
|
225
|
+
if win32gui.IsWindowVisible(hwnd):
|
|
226
|
+
window_title = win32gui.GetWindowText(hwnd)
|
|
227
|
+
if window_title:
|
|
228
|
+
windows.append((hwnd, window_title))
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
windows = []
|
|
232
|
+
win32gui.EnumWindows(enum_windows_callback, windows)
|
|
233
|
+
|
|
234
|
+
if not windows:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
titles = [title for hwnd, title in windows]
|
|
238
|
+
best_match = process.extractOne(title_query, titles, scorer=fuzz.token_set_ratio)
|
|
239
|
+
|
|
240
|
+
if best_match[1] >= threshold:
|
|
241
|
+
matched_title = best_match[0]
|
|
242
|
+
for hwnd, title in windows:
|
|
243
|
+
if title == matched_title:
|
|
244
|
+
return hwnd
|
|
245
|
+
return None
|
|
246
|
+
else:
|
|
247
|
+
def get_focused_window():
|
|
248
|
+
print("get_focused_window() only supports Windows! Returning None")
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def get_window_rect(hwnd):
|
|
252
|
+
print("get_window_rect() only supports Windows! Returning None")
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
def set_window_pos(hwnd, x: int, y: int, w: int, h: int):
|
|
256
|
+
print("set_window_pos() only supports Windows! Functionality disabled")
|
|
257
|
+
|
|
258
|
+
def find_window_by_title_fuzzy(title_query, threshold=70):
|
|
259
|
+
print("find_window_by_title_fuzzy() only supports Windows! Returning None")
|
|
260
|
+
return None
|
zhmiscellany/macro.py
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import math
|
|
2
2
|
import random
|
|
3
3
|
import threading
|
|
4
|
+
import sys
|
|
4
5
|
|
|
5
6
|
from ._misc_supportfuncs import move_mouse, mouse_down, mouse_up, get_mouse_xy
|
|
6
7
|
import zhmiscellany.misc
|
|
7
8
|
import zhmiscellany.math
|
|
8
9
|
import zhmiscellany.string
|
|
9
10
|
import zhmiscellany.processing
|
|
10
|
-
import win32api, win32con, ctypes
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# Windows-specific imports
|
|
13
|
+
if sys.platform == "win32":
|
|
14
|
+
try:
|
|
15
|
+
import win32api, win32con, ctypes
|
|
16
|
+
WIN32_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
WIN32_AVAILABLE = False
|
|
19
|
+
print("Warning: Windows modules not available - macro functionality disabled")
|
|
20
|
+
else:
|
|
21
|
+
WIN32_AVAILABLE = False
|
|
13
22
|
|
|
23
|
+
import keyboard, kthread
|
|
14
24
|
import time
|
|
15
25
|
|
|
16
26
|
get_mouse_xy = get_mouse_xy
|
|
@@ -28,9 +38,15 @@ def click_pixel(x=None, y=None, click_duration=None, right_click=False, middle_c
|
|
|
28
38
|
y = x[1]
|
|
29
39
|
x = x[0]
|
|
30
40
|
|
|
31
|
-
def stochastic_round(
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
def stochastic_round(val):
|
|
42
|
+
negative = val < 0
|
|
43
|
+
if negative:
|
|
44
|
+
val = val * -1
|
|
45
|
+
floor = int(val)
|
|
46
|
+
out = floor + (random.random() < (val - floor))
|
|
47
|
+
if negative:
|
|
48
|
+
out = out * -1
|
|
49
|
+
return out
|
|
34
50
|
|
|
35
51
|
if type(x) == float:
|
|
36
52
|
x = stochastic_round(x)
|
|
@@ -99,13 +115,17 @@ def click_pixel(x=None, y=None, click_duration=None, right_click=False, middle_c
|
|
|
99
115
|
for point in animation_points:
|
|
100
116
|
click_pixel((round(point[0]), round(point[1])), act_start=False, act_end=False, click_end_duration=1/animation_fps, relative=relative)
|
|
101
117
|
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
if WIN32_AVAILABLE:
|
|
119
|
+
if ctrl:
|
|
120
|
+
win32api.keybd_event(win32con.VK_CONTROL, 0, 0, 0)
|
|
121
|
+
keys_down.append(win32con.VK_CONTROL)
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
if shift:
|
|
124
|
+
win32api.keybd_event(win32con.VK_SHIFT, 0, 0, 0)
|
|
125
|
+
keys_down.append(win32con.VK_SHIFT)
|
|
126
|
+
else:
|
|
127
|
+
if ctrl or shift:
|
|
128
|
+
print("Warning: Modifier keys not supported on this platform")
|
|
109
129
|
|
|
110
130
|
if x is not None and y is not None:
|
|
111
131
|
if not relative:
|
|
@@ -129,12 +149,13 @@ def click_pixel(x=None, y=None, click_duration=None, right_click=False, middle_c
|
|
|
129
149
|
if not (animation_time and relative):
|
|
130
150
|
targ = tuple(a + b for a, b in zip(get_mouse_xy(), (cx, cy)))
|
|
131
151
|
move_mouse(cx, cy, relative=relative)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
152
|
+
if ensure_movement:
|
|
153
|
+
for i in range(limit):
|
|
154
|
+
cur_pos = get_mouse_xy()
|
|
155
|
+
if cur_pos != targ:
|
|
156
|
+
move_mouse(max(-1, min(1, (targ[0]-cur_pos[0]))), max(-1, min(1, (targ[1]-cur_pos[1]))), relative=relative)
|
|
157
|
+
else:
|
|
158
|
+
break
|
|
138
159
|
|
|
139
160
|
if pre_click_duration:
|
|
140
161
|
if pre_click_wiggle:
|
|
@@ -166,8 +187,9 @@ def click_pixel(x=None, y=None, click_duration=None, right_click=False, middle_c
|
|
|
166
187
|
if act_end:
|
|
167
188
|
mouse_up(1)
|
|
168
189
|
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
if WIN32_AVAILABLE:
|
|
191
|
+
for key in keys_down:
|
|
192
|
+
win32api.keybd_event(key, 0, win32con.KEYEVENTF_KEYUP, 0)
|
|
171
193
|
|
|
172
194
|
if click_end_duration:
|
|
173
195
|
zhmiscellany.misc.high_precision_sleep(click_end_duration)
|
|
@@ -177,6 +199,10 @@ def click_pixel(x=None, y=None, click_duration=None, right_click=False, middle_c
|
|
|
177
199
|
|
|
178
200
|
|
|
179
201
|
def press_key_directinput(key, shift=False, act_start=True, act_end=True, key_hold_time=0):
|
|
202
|
+
if not WIN32_AVAILABLE:
|
|
203
|
+
print("press_key_directinput() only supports Windows! Functionality disabled")
|
|
204
|
+
return
|
|
205
|
+
|
|
180
206
|
import pydirectinput
|
|
181
207
|
pydirectinput.PAUSE = 0
|
|
182
208
|
pydirectinput.FAILSAFE = False
|
|
@@ -188,6 +214,10 @@ def press_key_directinput(key, shift=False, act_start=True, act_end=True, key_ho
|
|
|
188
214
|
|
|
189
215
|
|
|
190
216
|
def press_key(vk_code, shift=False, act_start=True, act_end=True, key_hold_time=0):
|
|
217
|
+
if not WIN32_AVAILABLE:
|
|
218
|
+
print("press_key() only supports Windows! Functionality disabled")
|
|
219
|
+
return
|
|
220
|
+
|
|
191
221
|
if shift:
|
|
192
222
|
win32api.keybd_event(win32con.VK_SHIFT, 0, 0, 0)
|
|
193
223
|
if act_start:
|
|
@@ -258,7 +288,23 @@ def type_string(text=None, delay=None, key_hold_time=None, vk_codes=None, combin
|
|
|
258
288
|
press_key(vk_code, False, act_start=False, act_end=True, key_hold_time=key_hold_time)
|
|
259
289
|
|
|
260
290
|
|
|
261
|
-
def
|
|
291
|
+
def is_key_pressed_async(vk_code):
|
|
292
|
+
"""
|
|
293
|
+
Async check if a key is currently pressed
|
|
294
|
+
vk_code: Virtual Key code (e.g., 0x41 for 'A', 0x1B for ESC)
|
|
295
|
+
Returns: True if pressed, False otherwise
|
|
296
|
+
"""
|
|
297
|
+
if not WIN32_AVAILABLE:
|
|
298
|
+
print("is_key_pressed_async() only supports Windows! Returning False")
|
|
299
|
+
return False
|
|
300
|
+
return win32api.GetAsyncKeyState(vk_code) & 0x8000 != 0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def scroll(amount, delay=None, post_scroll_delay=None):
|
|
304
|
+
if not WIN32_AVAILABLE:
|
|
305
|
+
print("scroll() only supports Windows! Functionality disabled")
|
|
306
|
+
return
|
|
307
|
+
|
|
262
308
|
def raw_scroll(amount):
|
|
263
309
|
# Constants for mouse input
|
|
264
310
|
INPUT_MOUSE = 0
|
|
@@ -303,12 +349,18 @@ def scroll(amount, delay=None):
|
|
|
303
349
|
direction = 1 if amount > 0 else -1
|
|
304
350
|
amount = amount*direction
|
|
305
351
|
for _ in range(amount):
|
|
306
|
-
raw_scroll(
|
|
352
|
+
raw_scroll(direction)
|
|
307
353
|
zhmiscellany.misc.high_precision_sleep(delay/amount)
|
|
354
|
+
if post_scroll_delay:
|
|
355
|
+
zhmiscellany.misc.high_precision_sleep(post_scroll_delay)
|
|
308
356
|
|
|
309
357
|
|
|
310
358
|
def get_mouse_buttons():
|
|
311
359
|
"""Returns a list of booleans [M1, M2, M3] indicating which mouse buttons are held down."""
|
|
360
|
+
if not WIN32_AVAILABLE:
|
|
361
|
+
print("get_mouse_buttons() only supports Windows! Returning [False, False, False]")
|
|
362
|
+
return [False, False, False]
|
|
363
|
+
|
|
312
364
|
VK_LBUTTON = 0x01 # Left mouse button (M1)
|
|
313
365
|
VK_RBUTTON = 0x02 # Right mouse button (M2)
|
|
314
366
|
VK_MBUTTON = 0x04 # Middle mouse button (M3)
|
|
@@ -419,27 +471,29 @@ def record_actions_to_code(RECORD_MOUSE_MOVEMENT=False, STOP_KEY='f9'):
|
|
|
419
471
|
"m = zhmiscellany.macro.click_pixel",
|
|
420
472
|
"k = zhmiscellany.macro.press_key_directinput",
|
|
421
473
|
"s = zhmiscellany.macro.scroll",
|
|
422
|
-
"",
|
|
474
|
+
"sleep = zhmiscellany.misc.high_precision_sleep",
|
|
423
475
|
"click_down_time = 1/30",
|
|
424
476
|
"click_release_time = 1/30",
|
|
425
477
|
"mouse_move_dly = 1/60",
|
|
426
478
|
"key_down_time = 1/30",
|
|
427
479
|
"scroll_dly = 1/30",
|
|
480
|
+
"post_scroll_dly = 1/10",
|
|
481
|
+
"pre_click_duration = 1/30",
|
|
428
482
|
"",
|
|
429
483
|
"pre_click_wiggle = True",
|
|
430
484
|
"",
|
|
431
485
|
"animation_time = 0.1",
|
|
432
486
|
"",
|
|
433
487
|
'print("Replaying actions in 3 seconds...")',
|
|
434
|
-
"
|
|
488
|
+
"sleep(3)",
|
|
435
489
|
""
|
|
436
490
|
]
|
|
437
491
|
|
|
438
492
|
last_time = start_time
|
|
439
|
-
skip_next =
|
|
493
|
+
skip_next = 0
|
|
440
494
|
for i, event in enumerate(events):
|
|
441
495
|
if skip_next:
|
|
442
|
-
skip_next
|
|
496
|
+
skip_next -= 1
|
|
443
497
|
continue
|
|
444
498
|
current_time = event['time']
|
|
445
499
|
try:
|
|
@@ -476,17 +530,31 @@ def record_actions_to_code(RECORD_MOUSE_MOVEMENT=False, STOP_KEY='f9'):
|
|
|
476
530
|
|
|
477
531
|
if next_event and next_event['action'] == 'click' and (next_event['x'], next_event['y']) == (x, y):
|
|
478
532
|
action_str = ''
|
|
479
|
-
skip_next =
|
|
533
|
+
skip_next = 1
|
|
480
534
|
|
|
481
|
-
code_lines.append(f"m(({x}, {y}), {button_str}{action_str}click_duration=click_down_time, click_end_duration=click_release_time,
|
|
535
|
+
code_lines.append(f"m(({x}, {y}), {button_str}{action_str}click_duration=click_down_time, click_end_duration=click_release_time, pre_click_duration=pre_click_duration, animation_time=animation_time)")
|
|
482
536
|
|
|
483
537
|
elif action == 'move':
|
|
484
538
|
x, y = event['x'], event['y']
|
|
485
539
|
code_lines.append(f"m(({x}, {y}), click_end_duration=mouse_move_dly)")
|
|
486
540
|
|
|
487
541
|
elif action == 'scroll':
|
|
488
|
-
dx, dy = event['dx'], event['dy']
|
|
489
|
-
|
|
542
|
+
x, y, dx, dy = event['x'], event['y'], event['dx'], event['dy']
|
|
543
|
+
j = i
|
|
544
|
+
count = 0
|
|
545
|
+
while True:
|
|
546
|
+
count += 1
|
|
547
|
+
j += 1
|
|
548
|
+
try:
|
|
549
|
+
t_event = events[j]
|
|
550
|
+
except:
|
|
551
|
+
t_event = None
|
|
552
|
+
if t_event and t_event['action'] == 'scroll' and t_event['dy'] == dy and (t_event['x'], t_event['y']) == (x, y):
|
|
553
|
+
skip_next += 1
|
|
554
|
+
else:
|
|
555
|
+
break
|
|
556
|
+
code_lines.append(f"m(({x}, {y}), click_end_duration=mouse_move_dly)")
|
|
557
|
+
code_lines.append(f"s({dy*count}, scroll_dly, post_scroll_dly)")
|
|
490
558
|
|
|
491
559
|
elif action in ('key_press', 'key_release'):
|
|
492
560
|
key = event['key']
|
|
@@ -513,6 +581,9 @@ def record_actions_to_code(RECORD_MOUSE_MOVEMENT=False, STOP_KEY='f9'):
|
|
|
513
581
|
action_str = value
|
|
514
582
|
break
|
|
515
583
|
|
|
584
|
+
if key_str == STOP_KEY:
|
|
585
|
+
break
|
|
586
|
+
|
|
516
587
|
code_lines.append(f"k('{key_str}', {action_str}key_hold_time=key_down_time)")
|
|
517
588
|
|
|
518
589
|
last_time = current_time
|
|
@@ -546,16 +617,19 @@ def record_actions_to_code(RECORD_MOUSE_MOVEMENT=False, STOP_KEY='f9'):
|
|
|
546
617
|
events.append({'action': 'click', 'x': x, 'y': y, 'button': button, 'pressed': pressed, 'time': time.time()})
|
|
547
618
|
|
|
548
619
|
def on_scroll(x, y, dx, dy):
|
|
549
|
-
|
|
550
|
-
events.append({'action': 'scroll', 'dx': dx, 'dy': dy, 'time': time.time()})
|
|
620
|
+
events.append({'action': 'scroll', 'x': x, 'y': y, 'dx': dx, 'dy': dy, 'time': time.time()})
|
|
551
621
|
|
|
552
622
|
def on_press(key):
|
|
553
623
|
events.append({'action': 'key_press', 'key': key, 'time': time.time()})
|
|
554
624
|
|
|
555
625
|
def on_release(key):
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
626
|
+
try:
|
|
627
|
+
print(key.name)
|
|
628
|
+
if key.name == STOP_KEY:
|
|
629
|
+
# Stop listeners
|
|
630
|
+
return False
|
|
631
|
+
except:
|
|
632
|
+
pass
|
|
559
633
|
events.append({'action': 'key_release', 'key': key, 'time': time.time()})
|
|
560
634
|
|
|
561
635
|
print(f"Press '{STOP_KEY}' to stop recording and generate the script.")
|
|
@@ -720,5 +794,7 @@ KEY_CODES = {
|
|
|
720
794
|
'StartMailKey': 180,
|
|
721
795
|
'SelectMedia': 181,
|
|
722
796
|
'StartApplication1': 182,
|
|
723
|
-
'StartApplication2': 183
|
|
797
|
+
'StartApplication2': 183,
|
|
798
|
+
'Equals': 187,
|
|
799
|
+
'Minus': 189,
|
|
724
800
|
}
|
zhmiscellany/misc.py
CHANGED
|
@@ -7,7 +7,14 @@ import zhmiscellany.fileio
|
|
|
7
7
|
import time, hashlib, ctypes
|
|
8
8
|
import random, string, copy
|
|
9
9
|
import builtins, inspect
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
WIN32_AVAILABLE = False
|
|
12
|
+
if sys.platform == "win32":
|
|
13
|
+
try:
|
|
14
|
+
import win32gui, win32con, win32process
|
|
15
|
+
WIN32_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
print("Warning: pywin32 not available, Windows-specific features disabled")
|
|
11
18
|
|
|
12
19
|
import psutil
|
|
13
20
|
|
|
@@ -22,6 +29,9 @@ KEY_CODES = zhmiscellany.macro.KEY_CODES
|
|
|
22
29
|
|
|
23
30
|
|
|
24
31
|
def get_actual_screen_resolution():
|
|
32
|
+
if not WIN32_AVAILABLE:
|
|
33
|
+
print("get_actual_screen_resolution only supports Windows! Returning (0, 0)")
|
|
34
|
+
return (0, 0)
|
|
25
35
|
hdc = ctypes.windll.user32.GetDC(0)
|
|
26
36
|
width = ctypes.windll.gdi32.GetDeviceCaps(hdc, 118) # HORZRES
|
|
27
37
|
height = ctypes.windll.gdi32.GetDeviceCaps(hdc, 117) # VERTRES
|
|
@@ -30,6 +40,10 @@ def get_actual_screen_resolution():
|
|
|
30
40
|
|
|
31
41
|
|
|
32
42
|
def focus_window(process_name: str, interval=0):
|
|
43
|
+
if not WIN32_AVAILABLE:
|
|
44
|
+
print("focus_window only supports Windows!")
|
|
45
|
+
return
|
|
46
|
+
|
|
33
47
|
# Import user32.dll for additional window handling
|
|
34
48
|
user32 = ctypes.windll.user32
|
|
35
49
|
kernel32 = ctypes.windll.kernel32
|
|
@@ -116,6 +130,10 @@ def focus_window(process_name: str, interval=0):
|
|
|
116
130
|
|
|
117
131
|
|
|
118
132
|
def setup_console_window(xy=(0, 0), wh=(400, 100), always_on_top=True):
|
|
133
|
+
if not WIN32_AVAILABLE:
|
|
134
|
+
print("setup_console_window only supports Windows!")
|
|
135
|
+
return
|
|
136
|
+
|
|
119
137
|
# Get the console window handle
|
|
120
138
|
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
|
|
121
139
|
ontop = win32con.HWND_NOTOPMOST
|
|
@@ -223,6 +241,9 @@ def high_precision_sleep(duration):
|
|
|
223
241
|
|
|
224
242
|
|
|
225
243
|
def is_admin():
|
|
244
|
+
if not WIN32_AVAILABLE:
|
|
245
|
+
print("is_admin only supports Windows! Returning False")
|
|
246
|
+
return False
|
|
226
247
|
try:
|
|
227
248
|
return ctypes.windll.shell32.IsUserAnAdmin() == 1
|
|
228
249
|
except Exception:
|
|
@@ -749,4 +770,7 @@ l = types.FunctionType(
|
|
|
749
770
|
|
|
750
771
|
|
|
751
772
|
def wait_for_vsync():
|
|
752
|
-
|
|
773
|
+
if not WIN32_AVAILABLE:
|
|
774
|
+
print("wait_for_vsync only supports Windows!")
|
|
775
|
+
return
|
|
776
|
+
ctypes.windll.dwmapi.DwmFlush()
|
zhmiscellany/netio.py
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import os, requests
|
|
2
2
|
import zhmiscellany.string
|
|
3
3
|
import urllib.parse
|
|
4
|
-
|
|
4
|
+
import sys
|
|
5
5
|
from urllib3.exceptions import InsecureRequestWarning
|
|
6
6
|
import urllib3
|
|
7
7
|
|
|
8
8
|
urllib3.disable_warnings(InsecureRequestWarning)
|
|
9
9
|
|
|
10
|
+
WIN32_AVAILABLE = False
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
try:
|
|
13
|
+
from zhmiscellany._misc_supportfuncs import patch_rhg
|
|
14
|
+
from random_header_generator import HeaderGenerator
|
|
15
|
+
WIN32_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
print("Warning: random_header_generator not available, Windows-specific features disabled")
|
|
18
|
+
|
|
10
19
|
|
|
11
20
|
def resolve_file(url, destination_folder="."):
|
|
12
21
|
file_name = urllib.parse.unquote(url.split("/")[-1])
|
|
@@ -45,13 +54,12 @@ def download_file(url, destination_folder=".", just_return_path=False, headers=N
|
|
|
45
54
|
|
|
46
55
|
|
|
47
56
|
def generate_headers(url):
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
headers[k] = v
|
|
57
|
+
headers = {}
|
|
58
|
+
|
|
59
|
+
if WIN32_AVAILABLE:
|
|
60
|
+
generator = HeaderGenerator()
|
|
61
|
+
for k, v in generator().items():
|
|
62
|
+
headers[k] = v
|
|
55
63
|
|
|
56
64
|
headers['Referer'] = url
|
|
57
65
|
headers['Host'] = urllib.parse.urlparse(url).netloc
|
zhmiscellany/pipes.py
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import threading
|
|
2
2
|
import queue
|
|
3
3
|
import time
|
|
4
|
-
|
|
5
|
-
import win32pipe
|
|
6
|
-
import win32file
|
|
4
|
+
import sys
|
|
7
5
|
import zhmiscellany.string
|
|
8
6
|
|
|
7
|
+
WIN32_AVAILABLE = False
|
|
8
|
+
if sys.platform == "win32":
|
|
9
|
+
try:
|
|
10
|
+
import win32pipe
|
|
11
|
+
import win32file
|
|
12
|
+
WIN32_AVAILABLE = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
print("Warning: pywin32 not available, Windows pipe features disabled")
|
|
15
|
+
|
|
9
16
|
|
|
10
17
|
class PipeTransmitter:
|
|
11
18
|
def __init__(self, pipe_name, close_pipes=False):
|
|
19
|
+
if not WIN32_AVAILABLE:
|
|
20
|
+
print("PipeTransmitter only supports Windows!")
|
|
21
|
+
return
|
|
22
|
+
|
|
12
23
|
self.pipe_name = r'\\.\pipe'+'\\'+pipe_name
|
|
13
24
|
self.close_pipes = close_pipes
|
|
14
25
|
self.send_queue = queue.Queue()
|
|
@@ -18,6 +29,9 @@ class PipeTransmitter:
|
|
|
18
29
|
self.send_thread.start()
|
|
19
30
|
|
|
20
31
|
def send_data_thread(self):
|
|
32
|
+
if not WIN32_AVAILABLE:
|
|
33
|
+
return
|
|
34
|
+
|
|
21
35
|
pipe_handle = win32pipe.CreateNamedPipe(
|
|
22
36
|
self.pipe_name,
|
|
23
37
|
win32pipe.PIPE_ACCESS_OUTBOUND,
|
|
@@ -43,6 +57,10 @@ class PipeTransmitter:
|
|
|
43
57
|
|
|
44
58
|
class PipeReceiver:
|
|
45
59
|
def __init__(self, pipe_name):
|
|
60
|
+
if not WIN32_AVAILABLE:
|
|
61
|
+
print("PipeReceiver only supports Windows!")
|
|
62
|
+
return
|
|
63
|
+
|
|
46
64
|
self.pipe_name = r'\\.\pipe'+'\\'+pipe_name
|
|
47
65
|
self.receive_queue = queue.Queue()
|
|
48
66
|
self.callback_function = None
|
|
@@ -52,6 +70,9 @@ class PipeReceiver:
|
|
|
52
70
|
self.receive_thread.start()
|
|
53
71
|
|
|
54
72
|
def receive_data_thread(self):
|
|
73
|
+
if not WIN32_AVAILABLE:
|
|
74
|
+
return
|
|
75
|
+
|
|
55
76
|
pipe_handle = win32file.CreateFile(
|
|
56
77
|
self.pipe_name,
|
|
57
78
|
win32file.GENERIC_READ,
|
|
@@ -79,6 +100,10 @@ class PipeReceiver:
|
|
|
79
100
|
|
|
80
101
|
|
|
81
102
|
def raw_receive_data(pipe_name):
|
|
103
|
+
if not WIN32_AVAILABLE:
|
|
104
|
+
print("raw_receive_data only supports Windows! Returning None")
|
|
105
|
+
return None
|
|
106
|
+
|
|
82
107
|
try:
|
|
83
108
|
pipe_name = r'\\.\pipe' + '\\' + pipe_name
|
|
84
109
|
pipe_handle = win32file.CreateFile(
|
|
@@ -96,6 +121,10 @@ def raw_receive_data(pipe_name):
|
|
|
96
121
|
|
|
97
122
|
|
|
98
123
|
def raw_send_data(data, pipe_name):
|
|
124
|
+
if not WIN32_AVAILABLE:
|
|
125
|
+
print("raw_send_data only supports Windows!")
|
|
126
|
+
return
|
|
127
|
+
|
|
99
128
|
def _raw_send_data(data, pipe_name):
|
|
100
129
|
sent = False
|
|
101
130
|
while not sent:
|
|
@@ -117,4 +146,4 @@ def raw_send_data(data, pipe_name):
|
|
|
117
146
|
except:
|
|
118
147
|
time.sleep(1)
|
|
119
148
|
|
|
120
|
-
threading.Thread(target=_raw_send_data, args=(data, pipe_name)).start()
|
|
149
|
+
threading.Thread(target=_raw_send_data, args=(data, pipe_name)).start()
|
zhmiscellany/processing.py
CHANGED
|
@@ -279,4 +279,19 @@ if __name__=="__main__":
|
|
|
279
279
|
yield var[1]
|
|
280
280
|
proc.stdout.close()
|
|
281
281
|
proc.wait()
|
|
282
|
-
cleanup()
|
|
282
|
+
cleanup()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class thread_join_return(threading.Thread):
|
|
286
|
+
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
|
|
287
|
+
super().__init__(group, target, name, args, kwargs)
|
|
288
|
+
self._return = None
|
|
289
|
+
|
|
290
|
+
def run(self):
|
|
291
|
+
if self._target is not None:
|
|
292
|
+
self._return = self._target(*self._args, **self._kwargs)
|
|
293
|
+
|
|
294
|
+
# A custom 'join' that also returns the value
|
|
295
|
+
def join(self, *args):
|
|
296
|
+
super().join(*args)
|
|
297
|
+
return self._return
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: zhmiscellany
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.0.9
|
|
4
4
|
Summary: A collection of useful/interesting python libraries made by zh.
|
|
5
5
|
Home-page: https://discord.gg/ThBBAuueVJ
|
|
6
6
|
Author: zh
|
|
@@ -10,19 +10,20 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Classifier: Operating System :: Microsoft :: Windows
|
|
11
11
|
Requires-Python: >=3.6
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: ray>=0
|
|
14
13
|
Requires-Dist: pycryptodome>=0
|
|
15
|
-
Requires-Dist: pywin32>=0
|
|
16
14
|
Requires-Dist: discum==1.1.0
|
|
17
15
|
Requires-Dist: requests>=0
|
|
18
|
-
Requires-Dist: random-header-generator>=0
|
|
19
16
|
Requires-Dist: dill>=0
|
|
20
17
|
Requires-Dist: numpy>=0
|
|
21
18
|
Requires-Dist: keyboard>=0
|
|
22
19
|
Requires-Dist: psutil>=0
|
|
23
20
|
Requires-Dist: kthread>=0
|
|
24
|
-
Requires-Dist: pydirectinput>=0
|
|
25
21
|
Requires-Dist: pillow>=0
|
|
22
|
+
Requires-Dist: fuzzywuzzy>=0
|
|
23
|
+
Requires-Dist: ray>=0; sys_platform == "win32"
|
|
24
|
+
Requires-Dist: pywin32>=0; sys_platform == "win32"
|
|
25
|
+
Requires-Dist: random-header-generator>=0; sys_platform == "win32"
|
|
26
|
+
Requires-Dist: pydirectinput>=0; sys_platform == "win32"
|
|
26
27
|
|
|
27
28
|
`zhmiscellany`,
|
|
28
29
|
=
|
|
@@ -41,7 +42,7 @@ Introduction
|
|
|
41
42
|
|
|
42
43
|
Can be installed with `pip install zhmiscellany`
|
|
43
44
|
|
|
44
|
-
Currently, the package stands at
|
|
45
|
+
Currently, the package stands at 148 functions/classes/bindings across 15 modules.
|
|
45
46
|
|
|
46
47
|
The git repository for this package can be found [here](https://github.com/zen-ham/zhmiscellany). The docs also look nicer on github.
|
|
47
48
|
|
|
@@ -1348,6 +1349,15 @@ Very efficiently deduplicates an iterable such as a list and returns a list.
|
|
|
1348
1349
|
|
|
1349
1350
|
#
|
|
1350
1351
|
|
|
1352
|
+
`zhmiscellany.processing.thread_join_return()`
|
|
1353
|
+
---
|
|
1354
|
+
|
|
1355
|
+
`zhmiscellany.processing.thread_join_return()`
|
|
1356
|
+
|
|
1357
|
+
Just a thread, but it returns whatever value was returned by the function inside it when .join() is called so you can use the data.
|
|
1358
|
+
|
|
1359
|
+
#
|
|
1360
|
+
|
|
1351
1361
|
---
|
|
1352
1362
|
`zhmiscellany.string`
|
|
1353
1363
|
---
|
|
@@ -1527,6 +1537,24 @@ keyboard.wait() requires a clean press of the specified key, and concurrent key
|
|
|
1527
1537
|
|
|
1528
1538
|
#
|
|
1529
1539
|
|
|
1540
|
+
`zhmiscellany.macro.record_actions_to_code()`
|
|
1541
|
+
---
|
|
1542
|
+
|
|
1543
|
+
`zhmiscellany.macro.record_actions_to_code(RECORD_MOUSE_MOVEMENT=False, STOP_KEY='f9')`
|
|
1544
|
+
|
|
1545
|
+
Records keyboard and mouse events and generates zhmiscellany.macro code to emulate them, I'm so tired..
|
|
1546
|
+
|
|
1547
|
+
#
|
|
1548
|
+
|
|
1549
|
+
`zhmiscellany.macro.is_key_pressed_async()`
|
|
1550
|
+
---
|
|
1551
|
+
|
|
1552
|
+
`zhmiscellany.macro.is_key_pressed_async(vk_code)`
|
|
1553
|
+
|
|
1554
|
+
See if a key is pressed.
|
|
1555
|
+
|
|
1556
|
+
#
|
|
1557
|
+
|
|
1530
1558
|
---
|
|
1531
1559
|
`zhmiscellany.cpp`
|
|
1532
1560
|
---
|
|
@@ -1634,3 +1662,39 @@ Sends data to a named pipe asynchronously using a background thread.
|
|
|
1634
1662
|
A lightweight Python class that displays a customizable, semi-transparent, click-through colored rectangle overlay on the screen for visual status indication, such as if you had a complex macro/automation script and wanted to be able to see what step it's currently on.
|
|
1635
1663
|
|
|
1636
1664
|
#
|
|
1665
|
+
|
|
1666
|
+
`zhmiscellany.gui.get_focused_window()`
|
|
1667
|
+
---
|
|
1668
|
+
|
|
1669
|
+
`zhmiscellany.gui.get_focused_window()`
|
|
1670
|
+
|
|
1671
|
+
Return the window that currently has the keyboard focus.
|
|
1672
|
+
|
|
1673
|
+
#
|
|
1674
|
+
|
|
1675
|
+
`zhmiscellany.gui.get_window_rect()`
|
|
1676
|
+
---
|
|
1677
|
+
|
|
1678
|
+
`zhmiscellany.gui.get_window_rect(hwnd)`
|
|
1679
|
+
|
|
1680
|
+
Return the bounding rectangle of a window.
|
|
1681
|
+
|
|
1682
|
+
#
|
|
1683
|
+
|
|
1684
|
+
`zhmiscellany.gui.set_window_pos()`
|
|
1685
|
+
---
|
|
1686
|
+
|
|
1687
|
+
`zhmiscellany.gui.set_window_pos(hwnd, x: int, y: int, w: int, h: int)`
|
|
1688
|
+
|
|
1689
|
+
Move (and optionally resize) a window.
|
|
1690
|
+
|
|
1691
|
+
#
|
|
1692
|
+
|
|
1693
|
+
`zhmiscellany.gui.find_window_by_title_fuzzy()`
|
|
1694
|
+
---
|
|
1695
|
+
|
|
1696
|
+
`zhmiscellany.gui.find_window_by_title_fuzzy(title_query, threshold=70)`
|
|
1697
|
+
|
|
1698
|
+
Find a window HWND by rough name.
|
|
1699
|
+
|
|
1700
|
+
#
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
zhmiscellany/__init__.py,sha256=Oh3gAJRWq2aUEgkolmQyiZOQ3iey5OC4NA2XaTHuVv4,1139
|
|
2
2
|
zhmiscellany/_discord_supportfuncs.py,sha256=RotSpurqFtL5q6v_ST1e2Y_1qJMaHp1CDsF5i-gR4ec,6524
|
|
3
|
-
zhmiscellany/_fileio_supportfuncs.py,sha256=
|
|
4
|
-
zhmiscellany/_misc_supportfuncs.py,sha256=
|
|
5
|
-
zhmiscellany/_processing_supportfuncs.py,sha256=
|
|
3
|
+
zhmiscellany/_fileio_supportfuncs.py,sha256=soibLv9nOR79sHQ2MGQH2O6MhnqdXqI7ybxHSN2Tfac,434
|
|
4
|
+
zhmiscellany/_misc_supportfuncs.py,sha256=zD7LjbRCUC3wMuCS90jum8m_mNw1ZmW7ejPl7IZ9Nbg,11486
|
|
5
|
+
zhmiscellany/_processing_supportfuncs.py,sha256=u5dzW-2OtyViC74r5fhqd5_2uQ92ZhieRJRTJMevqbg,11071
|
|
6
6
|
zhmiscellany/_py_resources.py,sha256=HqJs5aRLymLZ2J5Io8g6bY58pGWhN9b8vCktC2DW4KQ,229009
|
|
7
7
|
zhmiscellany/_resource_files_lookup.py,sha256=hsgPW0dngokgqWrAVAv5rUo-eobzJPjvWHt4xrOG5l0,856
|
|
8
8
|
zhmiscellany/cpp.py,sha256=XEUEoIKCDCdY5VgwNWE5oXrGjtsmGdz_MnaVwQmi2dk,179
|
|
9
9
|
zhmiscellany/dict.py,sha256=0BZJ5eK-MurAHYV1OPa0jdGTr-QEWos7ZM0npb-tN9I,81
|
|
10
|
-
zhmiscellany/discord.py,sha256=
|
|
10
|
+
zhmiscellany/discord.py,sha256=nzXjRnJbuYbuH4hScqqdsF61VeXx8xiYrwp_AKTWt9o,19774
|
|
11
11
|
zhmiscellany/fileio.py,sha256=KmRnWWZJWBc5Or3_mKcRY2ehE_Fuk870iS-imtyERyg,18169
|
|
12
|
-
zhmiscellany/gui.py,sha256=
|
|
12
|
+
zhmiscellany/gui.py,sha256=8aOd4raEmaQJOOhr1CeVriYIOvy-Bw2FcoZMpKKlBng,10158
|
|
13
13
|
zhmiscellany/image.py,sha256=qUjxiYpc2VVZp2vwr1vN36O2PVQ7YlMKzhegQ1u4c0M,8198
|
|
14
14
|
zhmiscellany/list.py,sha256=S8Z85bLJEP9lk2JkGpzUcG6kpRB7a-NWDIHMPiF5bKo,1473
|
|
15
|
-
zhmiscellany/macro.py,sha256=
|
|
15
|
+
zhmiscellany/macro.py,sha256=kKtKYoHLSnBBHyGl8FlnfMaAoraKKSK023VA-mOezO0,27861
|
|
16
16
|
zhmiscellany/math.py,sha256=btOQTe_GvqP0A7Zz84tmN_c8j1NGe_mKnhmAt40lhLU,2482
|
|
17
|
-
zhmiscellany/misc.py,sha256=
|
|
18
|
-
zhmiscellany/netio.py,sha256=
|
|
17
|
+
zhmiscellany/misc.py,sha256=i4ZZ4ivfSfzjqXPe0etrK9l9zXk-TK6I9HRtts63YHc,30482
|
|
18
|
+
zhmiscellany/netio.py,sha256=VCqlo3ev_iOTTu5sXTZQwsYU0WMbAlOTDRsJ5Dj5-fc,2519
|
|
19
19
|
zhmiscellany/pastebin.py,sha256=TbZ3DqFYXo5qt5d95ugrofYoptlzKkjXUr7VnEqa6ks,6357
|
|
20
|
-
zhmiscellany/pipes.py,sha256=
|
|
21
|
-
zhmiscellany/processing.py,sha256=
|
|
20
|
+
zhmiscellany/pipes.py,sha256=zETvWP4PF-PuSzYwR1UCodY4ftNAgmCChb9DUMofXok,4657
|
|
21
|
+
zhmiscellany/processing.py,sha256=sDKIbzG9TNFyT6yJ4TJL59taG-59_v3CBLekVSLrwgE,10899
|
|
22
22
|
zhmiscellany/rust.py,sha256=znN6DYNoa_p-braTuDZKvUnXX8reWLFu_dG4fv2vLR0,442
|
|
23
23
|
zhmiscellany/string.py,sha256=xyqE6V5YF2nieZDcg5ZrXTIrH2D9oDRbZ5vQGz8rPys,4787
|
|
24
|
-
zhmiscellany-
|
|
25
|
-
zhmiscellany-
|
|
26
|
-
zhmiscellany-
|
|
27
|
-
zhmiscellany-
|
|
24
|
+
zhmiscellany-6.0.9.dist-info/METADATA,sha256=6XEuRXVoVBvCorsRSFU8Rm2ZVr81aYO8jVn8ZIAnwGY,43560
|
|
25
|
+
zhmiscellany-6.0.9.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
26
|
+
zhmiscellany-6.0.9.dist-info/top_level.txt,sha256=ioDtsrevCI52rTxZntMPljRIBsZs73tD0hI00HektiE,13
|
|
27
|
+
zhmiscellany-6.0.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|