zhmiscellany 5.7.9__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.
@@ -1,4 +1,8 @@
1
+ import sys
2
+
1
3
  def is_junction(entry):
4
+ if sys.platform != "win32":
5
+ return False
2
6
  try:
3
7
  st = entry.stat(follow_symlinks=False)
4
8
  # On Windows, st_file_attributes is available.
@@ -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 win32api, math
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
- patch_rhg()
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
- class INPUT(Structure):
100
- _fields_ = [("type", c_int),
101
- ("union", INPUT_UNION)]
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
- # Constants
105
- MOUSEEVENTF_ABSOLUTE = 0x8000
106
- MOUSEEVENTF_MOVE = 0x0001
107
- MOUSEEVENTF_LEFTDOWN = 0x0002
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
- # Screen metrics
158
+
159
+ # --- Screen Metrics ---
115
160
  def get_actual_screen_resolution():
116
- hdc = ctypes.windll.user32.GetDC(0)
117
- width = ctypes.windll.gdi32.GetDeviceCaps(hdc, 118) # HORZRES
118
- height = ctypes.windll.gdi32.GetDeviceCaps(hdc, 117) # VERTRES
119
- ctypes.windll.user32.ReleaseDC(0, hdc)
120
- return width, height
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 not relative:
129
- # Convert coordinates to normalized coordinates (0-65535)
130
- normalized_x = int(x * (65535 / SCREEN_WIDTH))
131
- normalized_y = int(y * (65535 / SCREEN_HEIGHT))
132
- else:
133
- calibrate()
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
- normalized_x = x
139
- normalized_y = y
140
-
141
- if not relative:
142
- dwflags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE
143
- else:
144
- dwflags = MOUSEEVENTF_MOVE
145
-
146
- input_struct = INPUT(
147
- type=0, # INPUT_MOUSE
148
- union=INPUT_UNION(
149
- mi=MOUSEINPUT(
150
- dx=normalized_x,
151
- dy=normalized_y,
152
- mouseData=0,
153
- dwFlags=dwflags,
154
- time=0,
155
- dwExtraInfo=None
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
- x, y = win32api.GetCursorPos()
164
- return x, y
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
- move_mouse(0,0)
177
- move_mouse(calibration_distance, calibration_distance, relative=True)
178
- moved_pos = get_mouse_xy()
179
- calibration_multiplier_x = calibration_distance/moved_pos[0]
180
- calibration_multiplier_y = calibration_distance/moved_pos[1]
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]+1, original_mouse_point[1]+1)
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
- flags = {
191
- 1: MOUSEEVENTF_LEFTDOWN,
192
- 2: MOUSEEVENTF_RIGHTDOWN,
193
- 3: MOUSEEVENTF_MIDDLEDOWN
194
- }
195
-
196
- input_struct = INPUT(
197
- type=0, # INPUT_MOUSE
198
- union=INPUT_UNION(
199
- mi=MOUSEINPUT(
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
- ctypes.windll.user32.SendInput(1, ctypes.byref(input_struct), sizeof(INPUT))
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
- flags = {
219
- 1: MOUSEEVENTF_LEFTUP,
220
- 2: MOUSEEVENTF_RIGHTUP,
221
- 3: MOUSEEVENTF_MIDDLEUP
222
- }
223
-
224
- input_struct = INPUT(
225
- type=0, # INPUT_MOUSE
226
- union=INPUT_UNION(
227
- mi=MOUSEINPUT(
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
- ctypes.windll.user32.SendInput(1, ctypes.byref(input_struct), sizeof(INPUT))
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
- import win32crypt
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 Crypto.Cipher.AES.new(win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1], Crypto.Cipher.AES.MODE_GCM, buff[3:15]).decrypt(buff[15:])[:-16].decode()
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 ctypes
4
- from ctypes import wintypes
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