windows-mcp 0.5.8__py3-none-any.whl → 0.5.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.
@@ -0,0 +1,490 @@
1
+ import comtypes.client
2
+ from comtypes import GUID, IUnknown, COMMETHOD, HRESULT
3
+ from ctypes import POINTER
4
+ from ctypes.wintypes import HWND, BOOL
5
+ import logging
6
+ import threading
7
+ import sys
8
+ import ctypes
9
+ from ctypes import HRESULT, POINTER, c_void_p, c_uint32, byref
10
+ from ctypes.wintypes import HWND, BOOL, UINT, INT, WCHAR, LPVOID
11
+ from comtypes import GUID, IUnknown, COMMETHOD, STDMETHOD
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ import secrets
16
+ import string
17
+
18
+ _thread_local = threading.local()
19
+
20
+ # Global mapping for Simple ID (Short) <-> GUID (Long)
21
+ _SIMPLE_TO_GUID = {}
22
+ _GUID_TO_SIMPLE = {}
23
+ _MAP_LOCK = threading.Lock()
24
+
25
+ def _get_simple_id(guid_str: str) -> str:
26
+ """Gets or creates a simple 8-char ID for a given GUID string."""
27
+ guid_str = guid_str.upper() # Normalize GUIDs
28
+ with _MAP_LOCK:
29
+ if guid_str in _GUID_TO_SIMPLE:
30
+ return _GUID_TO_SIMPLE[guid_str]
31
+
32
+ # Generate unique short ID
33
+ while True:
34
+ # 8 char random hex-like string (easy to read)
35
+ alphabet = string.ascii_lowercase + string.digits
36
+ short_id = ''.join(secrets.choice(alphabet) for _ in range(8))
37
+ if short_id not in _SIMPLE_TO_GUID:
38
+ break
39
+
40
+ _SIMPLE_TO_GUID[short_id] = guid_str
41
+ _GUID_TO_SIMPLE[guid_str] = short_id
42
+ return short_id
43
+
44
+ def _resolve_guid(simple_id: str) -> str:
45
+ """Resolves a simple ID to its GUID string."""
46
+ with _MAP_LOCK:
47
+ if simple_id in _SIMPLE_TO_GUID:
48
+ return _SIMPLE_TO_GUID[simple_id]
49
+ # Fallback: maybe the user passed a real GUID?
50
+ return simple_id
51
+
52
+ def _get_manager():
53
+ if not hasattr(_thread_local, "manager"):
54
+ _thread_local.manager = VirtualDesktopManager()
55
+ return _thread_local.manager
56
+
57
+ def is_window_on_current_desktop(hwnd: int) -> bool:
58
+ return _get_manager().is_window_on_current_desktop(hwnd)
59
+
60
+ def get_window_desktop_id(hwnd: int) -> str:
61
+ return _get_manager().get_window_desktop_id(hwnd)
62
+
63
+ def move_window_to_desktop(hwnd: int, desktop_id: str):
64
+ _get_manager().move_window_to_desktop(hwnd, desktop_id)
65
+
66
+ # standard COM CLSIDs for VirtualDesktopManager
67
+ CLSID_VirtualDesktopManager = GUID("{aa509086-5ca9-4c25-8f95-589d3c07b48a}")
68
+
69
+ class IVirtualDesktopManager(IUnknown):
70
+ _iid_ = GUID("{a5cd92ff-29be-454c-8d04-d82879fb3f1b}")
71
+ _methods_ = [
72
+ COMMETHOD([], HRESULT, "IsWindowOnCurrentVirtualDesktop",
73
+ (['in'], HWND, "topLevelWindow"),
74
+ (['out', 'retval'], POINTER(BOOL), "onCurrentDesktop")),
75
+ COMMETHOD([], HRESULT, "GetWindowDesktopId",
76
+ (['in'], HWND, "topLevelWindow"),
77
+ (['out', 'retval'], POINTER(GUID), "desktopId")),
78
+ COMMETHOD([], HRESULT, "MoveWindowToDesktop",
79
+ (['in'], HWND, "topLevelWindow"),
80
+ (['in'], GUID, "desktopId")),
81
+ ]
82
+
83
+ # Internal COM Interfaces for Windows 11
84
+ CLSID_ImmersiveShell = GUID("{C2F03A33-21F5-47FA-B4BB-156362A2F239}")
85
+ CLSID_VirtualDesktopManagerInternal = GUID("{C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B}")
86
+ IID_IServiceProvider = GUID("{6D5140C1-7436-11CE-8034-00AA006009FA}")
87
+
88
+ class IServiceProvider(IUnknown):
89
+ _iid_ = IID_IServiceProvider
90
+ _methods_ = [
91
+ COMMETHOD([], HRESULT, "QueryService",
92
+ (['in'], GUID, "guidService"),
93
+ (['in'], GUID, "riid"),
94
+ (['out'], POINTER(POINTER(IUnknown)), "ppvObject")),
95
+ ]
96
+
97
+ # IObjectArray for iterating desktops
98
+ class IObjectArray(IUnknown):
99
+ _iid_ = GUID("{92CA9DCD-5622-4BBA-A805-5E9F541BD8CC}")
100
+ _methods_ = [
101
+ COMMETHOD([], HRESULT, "GetCount",
102
+ (['out'], POINTER(UINT), "pcObjects")),
103
+ COMMETHOD([], HRESULT, "GetAt",
104
+ (['in'], UINT, "uiIndex"),
105
+ (['in'], GUID, "riid"),
106
+ (['out'], POINTER(POINTER(IUnknown)), "ppv")),
107
+ ]
108
+
109
+ # Wrapper for HSTRING
110
+ class HSTRING(c_void_p):
111
+ pass
112
+
113
+ try:
114
+ _combase = ctypes.windll.combase
115
+ _WindowsCreateString = _combase.WindowsCreateString
116
+ _WindowsCreateString.argtypes = [ctypes.c_wchar_p, UINT, POINTER(HSTRING)]
117
+ _WindowsCreateString.restype = HRESULT
118
+ _WindowsDeleteString = _combase.WindowsDeleteString
119
+ _WindowsDeleteString.argtypes = [HSTRING]
120
+ _WindowsDeleteString.restype = HRESULT
121
+ except Exception:
122
+ _WindowsCreateString = None
123
+ _WindowsDeleteString = None
124
+
125
+ def create_hstring(text):
126
+ if not _WindowsCreateString:
127
+ return HSTRING(0)
128
+ hs = HSTRING()
129
+ hr = _WindowsCreateString(text, len(text), byref(hs))
130
+ if hr != 0:
131
+ raise OSError(f"WindowsCreateString failed: {hr}")
132
+ return hs
133
+
134
+ def delete_hstring(hs):
135
+ if _WindowsDeleteString and hs:
136
+ _WindowsDeleteString(hs)
137
+
138
+ # Interface definitions depend on build number
139
+ BUILD = sys.getwindowsversion().build
140
+
141
+ if BUILD >= 26100:
142
+ IID_IVirtualDesktopManagerInternal = GUID("{53F5CA0B-158F-4124-900C-057158060B27}")
143
+ IID_IVirtualDesktop = GUID("{3F07F4BE-B107-441A-AF0F-39D82529072C}")
144
+ elif BUILD >= 22621:
145
+ IID_IVirtualDesktopManagerInternal = GUID("{A3175F2D-239C-4BD2-8AA0-EEBA8B0B138E}")
146
+ IID_IVirtualDesktop = GUID("{3F07F4BE-B107-441A-AF0F-39D82529072C}")
147
+ else:
148
+ # Windows 10 (Build 19041+)
149
+ IID_IVirtualDesktopManagerInternal = GUID("{F31574D6-B682-4CDC-BD56-1827860ABEC6}")
150
+ IID_IVirtualDesktop = GUID("{FF72FFDD-BE7E-43FC-9C03-AD81681E88E4}")
151
+
152
+ class IVirtualDesktop(IUnknown):
153
+ _iid_ = IID_IVirtualDesktop
154
+ # Methods for 22621+
155
+ _methods_ = [
156
+ STDMETHOD(HRESULT, "IsViewVisible", (POINTER(IUnknown), POINTER(UINT))), # IApplicationView
157
+ COMMETHOD([], HRESULT, "GetID", (['out'], POINTER(GUID), "pGuid")),
158
+ COMMETHOD([], HRESULT, "GetName", (['out'], POINTER(HSTRING), "pName")),
159
+ COMMETHOD([], HRESULT, "GetWallpaperPath", (['out'], POINTER(HSTRING), "pPath")),
160
+ COMMETHOD([], HRESULT, "IsRemote", (['out'], POINTER(HWND), "pW")),
161
+ ]
162
+
163
+ # Needed placeholders
164
+ class IApplicationView(IUnknown):
165
+ _iid_ = GUID("{372E1D3B-38D3-42E4-A15B-8AB2B178F513}") # Generic match
166
+
167
+ class IVirtualDesktopManagerInternal(IUnknown):
168
+ _iid_ = IID_IVirtualDesktopManagerInternal
169
+ if BUILD >= 26100:
170
+ _methods_ = [
171
+ COMMETHOD([], HRESULT, "GetCount", (['out'], POINTER(UINT), "pCount")),
172
+ STDMETHOD(HRESULT, "MoveViewToDesktop", (POINTER(IApplicationView), POINTER(IVirtualDesktop))),
173
+ STDMETHOD(HRESULT, "CanViewMoveDesktops", (POINTER(IApplicationView), POINTER(UINT))),
174
+ COMMETHOD([], HRESULT, "GetCurrentDesktop", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
175
+ COMMETHOD([], HRESULT, "GetDesktops", (['out'], POINTER(POINTER(IObjectArray)), "array")),
176
+ STDMETHOD(HRESULT, "GetAdjacentDesktop", (POINTER(IVirtualDesktop), UINT, POINTER(POINTER(IVirtualDesktop)))),
177
+ STDMETHOD(HRESULT, "SwitchDesktop", (POINTER(IVirtualDesktop),)),
178
+ STDMETHOD(HRESULT, "SwitchDesktopAndMoveForegroundView", (POINTER(IVirtualDesktop),)),
179
+ COMMETHOD([], HRESULT, "CreateDesktopW", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
180
+ STDMETHOD(HRESULT, "MoveDesktop", (POINTER(IVirtualDesktop), UINT)),
181
+ COMMETHOD([], HRESULT, "RemoveDesktop", (['in'], POINTER(IVirtualDesktop), "destroyDesktop"), (['in'], POINTER(IVirtualDesktop), "fallbackDesktop")),
182
+ COMMETHOD([], HRESULT, "FindDesktop", (['in'], POINTER(GUID), "pGuid"), (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
183
+ STDMETHOD(HRESULT, "GetDesktopSwitchIncludeExcludeViews", (POINTER(IVirtualDesktop), POINTER(POINTER(IObjectArray)), POINTER(POINTER(IObjectArray)))),
184
+ COMMETHOD([], HRESULT, "SetName", (['in'], POINTER(IVirtualDesktop), "pDesktop"), (['in'], HSTRING, "name")),
185
+ # Others omitted for now
186
+ ]
187
+ elif BUILD >= 22621:
188
+ _methods_ = [
189
+ COMMETHOD([], HRESULT, "GetCount", (['out'], POINTER(UINT), "pCount")),
190
+ STDMETHOD(HRESULT, "MoveViewToDesktop", (POINTER(IApplicationView), POINTER(IVirtualDesktop))),
191
+ STDMETHOD(HRESULT, "CanViewMoveDesktops", (POINTER(IApplicationView), POINTER(UINT))),
192
+ COMMETHOD([], HRESULT, "GetCurrentDesktop", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
193
+ COMMETHOD([], HRESULT, "GetDesktops", (['out'], POINTER(POINTER(IObjectArray)), "array")),
194
+ STDMETHOD(HRESULT, "GetAdjacentDesktop", (POINTER(IVirtualDesktop), UINT, POINTER(POINTER(IVirtualDesktop)))),
195
+ STDMETHOD(HRESULT, "SwitchDesktop", (POINTER(IVirtualDesktop),)),
196
+ COMMETHOD([], HRESULT, "CreateDesktopW", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
197
+ STDMETHOD(HRESULT, "MoveDesktop", (POINTER(IVirtualDesktop), UINT)),
198
+ COMMETHOD([], HRESULT, "RemoveDesktop", (['in'], POINTER(IVirtualDesktop), "destroyDesktop"), (['in'], POINTER(IVirtualDesktop), "fallbackDesktop")),
199
+ COMMETHOD([], HRESULT, "FindDesktop", (['in'], POINTER(GUID), "pGuid"), (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
200
+ STDMETHOD(HRESULT, "GetDesktopSwitchIncludeExcludeViews", (POINTER(IVirtualDesktop), POINTER(POINTER(IObjectArray)), POINTER(POINTER(IObjectArray)))),
201
+ COMMETHOD([], HRESULT, "SetName", (['in'], POINTER(IVirtualDesktop), "pDesktop"), (['in'], HSTRING, "name")),
202
+ ]
203
+ else:
204
+ # Windows 10
205
+ _methods_ = [
206
+ COMMETHOD([], HRESULT, "GetCount", (['out'], POINTER(UINT), "pCount")),
207
+ STDMETHOD(HRESULT, "MoveViewToDesktop", (POINTER(IApplicationView), POINTER(IVirtualDesktop))),
208
+ STDMETHOD(HRESULT, "CanViewMoveDesktops", (POINTER(IApplicationView), POINTER(UINT))),
209
+ COMMETHOD([], HRESULT, "GetCurrentDesktop", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
210
+ COMMETHOD([], HRESULT, "GetDesktops", (['out'], POINTER(POINTER(IObjectArray)), "array")),
211
+ STDMETHOD(HRESULT, "GetAdjacentDesktop", (POINTER(IVirtualDesktop), UINT, POINTER(POINTER(IVirtualDesktop)))),
212
+ STDMETHOD(HRESULT, "SwitchDesktop", (POINTER(IVirtualDesktop),)),
213
+ COMMETHOD([], HRESULT, "CreateDesktopW", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
214
+ COMMETHOD([], HRESULT, "RemoveDesktop", (['in'], POINTER(IVirtualDesktop), "destroyDesktop"), (['in'], POINTER(IVirtualDesktop), "fallbackDesktop")),
215
+ COMMETHOD([], HRESULT, "FindDesktop", (['in'], POINTER(GUID), "pGuid"), (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
216
+ # No SetName on this interface for Win10 typically
217
+ ]
218
+
219
+ class VirtualDesktopManager:
220
+ """
221
+ Wrapper around the Windows IVirtualDesktopManager interface.
222
+ Allows checking if a window is on the current virtual desktop, getting its Desktop ID,
223
+ and moving windows between desktops.
224
+ """
225
+ def __init__(self):
226
+ self._manager = None
227
+ try:
228
+ # Ensure COM is initialized on this thread (RPC_E_CHANGED_MODE or S_OK/S_FALSE)
229
+ # COINIT_APARTMENTTHREADED = 0x2, COINIT_MULTITHREADED = 0x0
230
+ # We try standard init.
231
+ import ctypes
232
+ try:
233
+ ctypes.windll.ole32.CoInitialize(None)
234
+ except Exception:
235
+ pass # Already initialized or failed, try proceeding
236
+
237
+ self._manager = comtypes.client.CreateObject(CLSID_VirtualDesktopManager, interface=IVirtualDesktopManager)
238
+
239
+ # Initialize Internal Manager
240
+ try:
241
+ service_provider = comtypes.client.CreateObject(CLSID_ImmersiveShell, interface=IServiceProvider)
242
+ unk = service_provider.QueryService(CLSID_VirtualDesktopManagerInternal, IVirtualDesktopManagerInternal._iid_)
243
+ self._internal_manager = unk.QueryInterface(IVirtualDesktopManagerInternal)
244
+ except Exception as e:
245
+ logger.warning(f"Failed to initialize VirtualDesktopManagerInternal: {e}")
246
+ self._internal_manager = None
247
+
248
+ except Exception as e:
249
+ logger.error(f"Failed to initialize VirtualDesktopManager: {e}")
250
+
251
+ def is_window_on_current_desktop(self, hwnd: int) -> bool:
252
+ """
253
+ Checks if the specified window is on the currently active virtual desktop.
254
+ """
255
+ if not self._manager:
256
+ return True # Fallback: assume visible if manager failed
257
+ try:
258
+ return self._manager.IsWindowOnCurrentVirtualDesktop(hwnd)
259
+ except Exception:
260
+ return True # Fail open
261
+
262
+ def get_window_desktop_id(self, hwnd: int) -> str:
263
+ """
264
+ Returns the GUID (as a string) of the virtual desktop the window is on.
265
+ """
266
+ if not self._manager:
267
+ return ""
268
+ try:
269
+ guid = self._manager.GetWindowDesktopId(hwnd)
270
+ return _get_simple_id(str(guid))
271
+ except Exception:
272
+ return ""
273
+
274
+ def move_window_to_desktop(self, hwnd: int, desktop_id: str):
275
+ """
276
+ Moves a window to the specified virtual desktop (by GUID string).
277
+ """
278
+ if not self._manager:
279
+ return
280
+ try:
281
+ target_guid_str = _resolve_guid(desktop_id)
282
+ guid = GUID(target_guid_str)
283
+ self._manager.MoveWindowToDesktop(hwnd, guid)
284
+ except Exception as e:
285
+ logger.error(f"Failed to move window to desktop: {e}")
286
+
287
+ def create_desktop(self, name: str = None) -> str:
288
+ """
289
+ Creates a new virtual desktop and returns its ID.
290
+ Optionally sets the name of the new desktop.
291
+ """
292
+ if not self._internal_manager:
293
+ raise RuntimeError("Internal VDM not initialized")
294
+
295
+ desktop = self._internal_manager.CreateDesktopW()
296
+ guid = desktop.GetID()
297
+ guid_str = str(guid)
298
+
299
+ # Get simple ID
300
+ simple_id = _get_simple_id(guid_str)
301
+
302
+ if name:
303
+ self.rename_desktop(simple_id, name) # Use simple ID internally too now
304
+
305
+ return simple_id
306
+
307
+ def remove_desktop(self, desktop_id: str):
308
+ """
309
+ Removes a virtual desktop by ID.
310
+ Will try to fallback to the first available desktop that is not the one being deleted.
311
+ """
312
+ if not self._internal_manager:
313
+ raise RuntimeError("Internal VDM not initialized")
314
+
315
+ target_guid_str = _resolve_guid(desktop_id)
316
+ target_guid = GUID(target_guid_str)
317
+
318
+ # We need the IVirtualDesktop object for the target
319
+ try:
320
+ target_desktop = self._internal_manager.FindDesktop(target_guid)
321
+ except Exception:
322
+ logger.error(f"Could not find desktop with ID {desktop_id}")
323
+ return
324
+
325
+ # Find a fallback desktop
326
+ desktops_array = self._internal_manager.GetDesktops()
327
+ count = desktops_array.GetCount()
328
+ fallback_desktop = None
329
+
330
+ for i in range(count):
331
+ unk = desktops_array.GetAt(i, IVirtualDesktop._iid_)
332
+ candidate = unk.QueryInterface(IVirtualDesktop)
333
+ candidate_id = candidate.GetID()
334
+ if str(candidate_id) != str(target_guid):
335
+ fallback_desktop = candidate
336
+ break
337
+
338
+ if not fallback_desktop:
339
+ # If no other desktop, we can't delete the only one? Or create one?
340
+ # Windows usually prevents deleting the last one.
341
+ logger.error("No fallback desktop found (cannot delete the only desktop)")
342
+ return
343
+
344
+ self._internal_manager.RemoveDesktop(target_desktop, fallback_desktop)
345
+
346
+ def rename_desktop(self, desktop_id: str, new_name: str):
347
+ """
348
+ Renames a virtual desktop.
349
+ """
350
+ if not self._internal_manager:
351
+ raise RuntimeError("Internal VDM not initialized")
352
+
353
+ target_guid_str = _resolve_guid(desktop_id)
354
+ target_guid = GUID(target_guid_str)
355
+ try:
356
+ target_desktop = self._internal_manager.FindDesktop(target_guid)
357
+ except Exception:
358
+ logger.error(f"Could not find desktop with ID {desktop_id}")
359
+ return
360
+
361
+ hs_name = create_hstring(new_name)
362
+ try:
363
+ # Check if SetName method exists (it might not on Windows 10)
364
+ if hasattr(self._internal_manager, "SetName"):
365
+ self._internal_manager.SetName(target_desktop, hs_name)
366
+ else:
367
+ logger.warning("Rename desktop is not supported on this Windows build.")
368
+ except Exception as e:
369
+ logger.error(f"Failed to rename desktop: {e}")
370
+ finally:
371
+ delete_hstring(hs_name)
372
+
373
+ def switch_desktop(self, desktop_id: str):
374
+ """
375
+ Switches to the specified virtual desktop.
376
+ """
377
+ if not self._internal_manager:
378
+ raise RuntimeError("Internal VDM not initialized")
379
+
380
+ target_guid_str = _resolve_guid(desktop_id)
381
+ target_guid = GUID(target_guid_str)
382
+ try:
383
+ target_desktop = self._internal_manager.FindDesktop(target_guid)
384
+ except Exception:
385
+ logger.error(f"Could not find desktop with ID {desktop_id}")
386
+ return
387
+
388
+ self._internal_manager.SwitchDesktop(target_desktop)
389
+
390
+ def get_all_desktops(self) -> list[dict]:
391
+ """
392
+ Returns a list of all virtual desktops.
393
+ Each entry is a dict: {'id': str, 'name': str}
394
+ """
395
+ if not self._internal_manager:
396
+ raise RuntimeError("Internal VDM not initialized")
397
+
398
+ desktops_array = self._internal_manager.GetDesktops()
399
+ count = desktops_array.GetCount()
400
+
401
+ result = []
402
+ for i in range(count):
403
+ try:
404
+ unk = desktops_array.GetAt(i, IVirtualDesktop._iid_)
405
+ desktop = unk.QueryInterface(IVirtualDesktop)
406
+ guid = desktop.GetID()
407
+ simple_id = _get_simple_id(str(guid))
408
+
409
+ name = ""
410
+ try:
411
+ # Windows 10 interface might not support GetName commonly or it might fail
412
+ if hasattr(desktop, "GetName"):
413
+ hname = desktop.GetName()
414
+ name = ctypes.wstring_at(hname)
415
+ delete_hstring(hname)
416
+ except Exception:
417
+ pass # Name retrieval failed, ignore
418
+
419
+ if not name:
420
+ name = f"Desktop {i+1}"
421
+
422
+ result.append({'id': simple_id, 'name': name})
423
+ except Exception as e:
424
+ logger.error(f"Error retrieving desktop at index {i}: {e}")
425
+ continue
426
+
427
+ return result
428
+
429
+
430
+ def get_current_desktop(self) -> dict:
431
+ """
432
+ Returns info about the current virtual desktop.
433
+ Returns: {'id': str, 'name': str}
434
+ """
435
+ if not self._internal_manager:
436
+ raise RuntimeError("Internal VDM not initialized")
437
+
438
+ current_desktop = self._internal_manager.GetCurrentDesktop()
439
+ guid = current_desktop.GetID()
440
+ simple_id = _get_simple_id(str(guid))
441
+
442
+ name = ""
443
+ try:
444
+ # Windows 10 interface might not support GetName commonly or it might fail
445
+ if hasattr(current_desktop, "GetName"):
446
+ hname = current_desktop.GetName()
447
+ name = ctypes.wstring_at(hname)
448
+ delete_hstring(hname)
449
+ except Exception:
450
+ pass
451
+
452
+ if not name:
453
+ # Fallback logic
454
+ desktops_array = self._internal_manager.GetDesktops()
455
+ count = desktops_array.GetCount()
456
+ current_guid_str = str(guid)
457
+
458
+ found_name = "Current Desktop"
459
+ for i in range(count):
460
+ try:
461
+ unk = desktops_array.GetAt(i, IVirtualDesktop._iid_)
462
+ candidate = unk.QueryInterface(IVirtualDesktop)
463
+ candidate_guid_str = str(candidate.GetID())
464
+ if candidate_guid_str == current_guid_str:
465
+ found_name = f"Desktop {i+1}"
466
+ break
467
+ except Exception:
468
+ continue
469
+ name = found_name
470
+
471
+ return {'id': simple_id, 'name': name}
472
+
473
+ def create_desktop(name: str = None) -> str:
474
+ return _get_manager().create_desktop(name)
475
+
476
+ def remove_desktop(desktop_id: str):
477
+ _get_manager().remove_desktop(desktop_id)
478
+
479
+ def rename_desktop(desktop_id: str, new_name: str):
480
+ _get_manager().rename_desktop(desktop_id, new_name)
481
+
482
+ def switch_desktop(desktop_id: str):
483
+ _get_manager().switch_desktop(desktop_id)
484
+
485
+ def get_all_desktops() -> list[dict]:
486
+ return _get_manager().get_all_desktops()
487
+
488
+ def get_current_desktop() -> dict:
489
+ return _get_manager().get_current_desktop()
490
+
@@ -1,5 +1,6 @@
1
1
  from windows_mcp.uia.core import _AutomationClient
2
2
  import comtypes
3
+ import weakref
3
4
 
4
5
  # Get UIA Interface for COM definitions
5
6
  uia_client = _AutomationClient.instance()
@@ -9,13 +10,14 @@ class FocusChangedEventHandler(comtypes.COMObject):
9
10
  _com_interfaces_ = [UIA.IUIAutomationFocusChangedEventHandler]
10
11
 
11
12
  def __init__(self, parent):
12
- self.parent = parent
13
+ self._parent = weakref.ref(parent)
13
14
  super(FocusChangedEventHandler, self).__init__()
14
15
 
15
16
  def HandleFocusChangedEvent(self, sender):
16
17
  try:
17
- if self.parent._focus_callback:
18
- self.parent._focus_callback(sender)
18
+ parent = self._parent()
19
+ if parent and parent._focus_callback:
20
+ parent._focus_callback(sender)
19
21
  except Exception as e:
20
22
  print(f"Error in focus callback: {e}")
21
23
  return 0 # S_OK
@@ -24,13 +26,14 @@ class StructureChangedEventHandler(comtypes.COMObject):
24
26
  _com_interfaces_ = [UIA.IUIAutomationStructureChangedEventHandler]
25
27
 
26
28
  def __init__(self, parent):
27
- self.parent = parent
29
+ self._parent = weakref.ref(parent)
28
30
  super(StructureChangedEventHandler, self).__init__()
29
31
 
30
32
  def HandleStructureChangedEvent(self, sender, changeType, runtimeId):
31
33
  try:
32
- if self.parent._structure_callback:
33
- self.parent._structure_callback(sender, changeType, runtimeId)
34
+ parent = self._parent()
35
+ if parent and parent._structure_callback:
36
+ parent._structure_callback(sender, changeType, runtimeId)
34
37
  except Exception as e:
35
38
  print(f"Error in structure callback: {e}")
36
39
  return 0 # S_OK
@@ -39,13 +42,14 @@ class PropertyChangedEventHandler(comtypes.COMObject):
39
42
  _com_interfaces_ = [UIA.IUIAutomationPropertyChangedEventHandler]
40
43
 
41
44
  def __init__(self, parent):
42
- self.parent = parent
45
+ self._parent = weakref.ref(parent)
43
46
  super(PropertyChangedEventHandler, self).__init__()
44
47
 
45
48
  def HandlePropertyChangedEvent(self, sender, propertyId, newValue):
46
49
  try:
47
- if self.parent._property_callback:
48
- self.parent._property_callback(sender, propertyId, newValue)
50
+ parent = self._parent()
51
+ if parent and parent._property_callback:
52
+ parent._property_callback(sender, propertyId, newValue)
49
53
  except Exception as e:
50
54
  print(f"Error in property callback: {e}")
51
55
  return 0 # S_OK
@@ -7,6 +7,8 @@ from windows_mcp.uia.enums import TreeScope
7
7
  from threading import Thread, Event
8
8
  import comtypes.client
9
9
  import comtypes
10
+ import logging
11
+ import weakref
10
12
 
11
13
  from .event_handlers import (
12
14
  FocusChangedEventHandler,
@@ -14,6 +16,9 @@ from .event_handlers import (
14
16
  PropertyChangedEventHandler
15
17
  )
16
18
 
19
+ logger = logging.getLogger(__name__)
20
+ logger.setLevel(logging.INFO)
21
+
17
22
  class WatchDog:
18
23
  def __init__(self):
19
24
  self.uia_client = _AutomationClient.instance()
@@ -90,12 +95,12 @@ class WatchDog:
90
95
  self._focus_handler = FocusChangedEventHandler(self)
91
96
  self.uia.AddFocusChangedEventHandler(None, self._focus_handler)
92
97
  except Exception as e:
93
- print(f"Failed to add focus handler: {e}")
98
+ logger.debug(f"Failed to add focus handler: {e}")
94
99
  elif not self._focus_callback and self._focus_handler:
95
100
  try:
96
101
  self.uia.RemoveFocusChangedEventHandler(self._focus_handler)
97
102
  except Exception as e:
98
- print(f"Failed to remove focus handler: {e}")
103
+ logger.debug(f"Failed to remove focus handler: {e}")
99
104
  self._focus_handler = None
100
105
 
101
106
  # --- Structure Monitoring ---
@@ -110,7 +115,7 @@ class WatchDog:
110
115
  target = self._active_structure_element if self._active_structure_element else self.uia.GetRootElement()
111
116
  self.uia.RemoveStructureChangedEventHandler(target, self._structure_handler)
112
117
  except Exception as e:
113
- print(f"Failed to remove structure handler: {e}")
118
+ logger.debug(f"Failed to remove structure handler: {e}")
114
119
  self._structure_handler = None
115
120
  self._active_structure_element = None
116
121
  is_active = False
@@ -124,7 +129,7 @@ class WatchDog:
124
129
  self.uia.AddStructureChangedEventHandler(target, scope, None, self._structure_handler)
125
130
  self._active_structure_element = target
126
131
  except Exception as e:
127
- print(f"Failed to add structure handler: {e}")
132
+ logger.debug(f"Failed to add structure handler: {e}")
128
133
 
129
134
  # --- Property Monitoring ---
130
135
  config_changed = (self._property_element != self._active_property_element) or \
@@ -172,17 +177,23 @@ class WatchDog:
172
177
  try:
173
178
  self.uia.RemoveFocusChangedEventHandler(self._focus_handler)
174
179
  except: pass
180
+ self._focus_handler = None
175
181
 
176
182
  if self._structure_handler:
177
183
  try:
178
184
  target = self._active_structure_element if self._active_structure_element else self.uia.GetRootElement()
179
185
  self.uia.RemoveStructureChangedEventHandler(target, self._structure_handler)
180
186
  except: pass
187
+ self._structure_handler = None
188
+ self._active_structure_element = None
181
189
 
182
190
  if self._property_handler:
183
191
  try:
184
192
  target = self._active_property_element if self._active_property_element else self.uia.GetRootElement()
185
193
  self.uia.RemovePropertyChangedEventHandler(target, self._property_handler)
186
194
  except: pass
195
+ self._property_handler = None
196
+ self._active_property_element = None
197
+ self._active_property_ids = None
187
198
 
188
199
  comtypes.CoUninitialize()